After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 15 KiB |
@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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.platform.concurrency
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A TaskRunner provides the ability of running tasks in different queues. The
|
||||||
|
* class is also observable, and notifies listeners when new tasks are started
|
||||||
|
* or finished.
|
||||||
|
*
|
||||||
|
* Two queues are available: a foreground queue and a background queue. These
|
||||||
|
* two queues may run in parallel, depending on the hardware. Multiple tasks
|
||||||
|
* submitted to the same queue, however, always run sequentially, in the order
|
||||||
|
* they were enqueued.
|
||||||
|
*/
|
||||||
|
interface TaskRunner {
|
||||||
|
|
||||||
|
val listeners: MutableList<Listener>
|
||||||
|
|
||||||
|
val activeTaskCount: Int
|
||||||
|
|
||||||
|
fun runInBackground(task: () -> Unit)
|
||||||
|
|
||||||
|
fun runInForeground(task: () -> Unit)
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun onTaskStarted()
|
||||||
|
fun onTaskFinished()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sequential implementation of TaskRunner. Both background and foreground
|
||||||
|
* queues run in the same thread, so they block each other.
|
||||||
|
*/
|
||||||
|
class SequentialTaskRunner : TaskRunner {
|
||||||
|
|
||||||
|
override val listeners = mutableListOf<TaskRunner.Listener>()
|
||||||
|
|
||||||
|
override var activeTaskCount = 0
|
||||||
|
|
||||||
|
override fun runInBackground(task: () -> Unit) {
|
||||||
|
activeTaskCount += 1
|
||||||
|
for (l in listeners) l.onTaskStarted()
|
||||||
|
task()
|
||||||
|
activeTaskCount -= 1
|
||||||
|
for (l in listeners) l.onTaskFinished()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun runInForeground(task: () -> Unit) = runInBackground(task)
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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.platform.gui
|
||||||
|
|
||||||
|
data class PaletteColor(val index: Int)
|
||||||
|
|
||||||
|
data class Color(val red: Double,
|
||||||
|
val green: Double,
|
||||||
|
val blue: Double,
|
||||||
|
val alpha: Double) {
|
||||||
|
|
||||||
|
val luminosity: Double
|
||||||
|
get() {
|
||||||
|
return 0.21 * red + 0.72 * green + 0.07 * blue
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(rgb: Int) : this(((rgb shr 16) and 0xFF) / 255.0,
|
||||||
|
((rgb shr 8) and 0xFF) / 255.0,
|
||||||
|
((rgb shr 0) and 0xFF) / 255.0,
|
||||||
|
1.0)
|
||||||
|
|
||||||
|
fun blendWith(other: Color, weight: Double): Color {
|
||||||
|
return Color(red * (1 - weight) + other.red * weight,
|
||||||
|
green * (1 - weight) + other.green * weight,
|
||||||
|
blue * (1 - weight) + other.blue * weight,
|
||||||
|
alpha * (1 - weight) + other.alpha * weight)
|
||||||
|
}
|
||||||
|
}
|
@ -1,82 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
|
|
||||||
import org.isoron.uhabits.gui.*
|
|
||||||
import org.isoron.uhabits.models.*
|
|
||||||
import org.isoron.uhabits.utils.*
|
|
||||||
|
|
||||||
|
|
||||||
class Backend(databaseOpener: DatabaseOpener,
|
|
||||||
fileOpener: FileOpener,
|
|
||||||
log: Log) {
|
|
||||||
|
|
||||||
private var database: Database
|
|
||||||
private var habitsRepository: HabitRepository
|
|
||||||
private var habits = mutableMapOf<Int, Habit>()
|
|
||||||
var theme: Theme = LightTheme()
|
|
||||||
|
|
||||||
init {
|
|
||||||
val dbFile = fileOpener.openUserFile("uhabits.db")
|
|
||||||
database = databaseOpener.open(dbFile)
|
|
||||||
database.migrateTo(LOOP_DATABASE_VERSION, fileOpener, log)
|
|
||||||
habitsRepository = HabitRepository(database)
|
|
||||||
habits = habitsRepository.findAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getHabitList(): List<Map<String, *>> {
|
|
||||||
return habits.values
|
|
||||||
.filter { h -> !h.isArchived }
|
|
||||||
.sortedBy { h -> h.position }
|
|
||||||
.map { h ->
|
|
||||||
mapOf("key" to h.id.toString(),
|
|
||||||
"name" to h.name,
|
|
||||||
"color" to h.color.index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createHabit(name: String) {
|
|
||||||
val id = habitsRepository.nextId()
|
|
||||||
val habit = Habit(id = id,
|
|
||||||
name = name,
|
|
||||||
description = "",
|
|
||||||
frequency = Frequency(1, 1),
|
|
||||||
color = PaletteColor(3),
|
|
||||||
isArchived = false,
|
|
||||||
position = habits.size,
|
|
||||||
unit = "",
|
|
||||||
target = 0.0,
|
|
||||||
type = HabitType.YES_NO_HABIT)
|
|
||||||
habitsRepository.insert(habit)
|
|
||||||
habits[id] = habit
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteHabit(id: Int) {
|
|
||||||
val habit = habits[id]!!
|
|
||||||
habitsRepository.delete(habit)
|
|
||||||
habits.remove(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateHabit(id: Int, name: String) {
|
|
||||||
val habit = habits[id]!!
|
|
||||||
habit.name = name
|
|
||||||
habitsRepository.update(habit)
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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.backend
|
||||||
|
|
||||||
|
import org.isoron.platform.concurrency.*
|
||||||
|
import org.isoron.platform.io.*
|
||||||
|
import org.isoron.platform.time.*
|
||||||
|
import org.isoron.uhabits.*
|
||||||
|
import org.isoron.uhabits.components.*
|
||||||
|
import org.isoron.uhabits.models.*
|
||||||
|
|
||||||
|
class Backend(databaseName: String,
|
||||||
|
databaseOpener: DatabaseOpener,
|
||||||
|
fileOpener: FileOpener,
|
||||||
|
val log: Log,
|
||||||
|
val dateCalculator: LocalDateCalculator,
|
||||||
|
val taskRunner: TaskRunner) {
|
||||||
|
|
||||||
|
val database: Database
|
||||||
|
|
||||||
|
val habitsRepository: HabitRepository
|
||||||
|
|
||||||
|
val checkmarkRepository: CheckmarkRepository
|
||||||
|
|
||||||
|
val habits = mutableMapOf<Int, Habit>()
|
||||||
|
|
||||||
|
val checkmarks = mutableMapOf<Habit, CheckmarkList>()
|
||||||
|
|
||||||
|
val mainScreenDataSource: MainScreenDataSource
|
||||||
|
|
||||||
|
var theme: Theme = LightTheme()
|
||||||
|
|
||||||
|
init {
|
||||||
|
val dbFile = fileOpener.openUserFile(databaseName)
|
||||||
|
if (!dbFile.exists()) {
|
||||||
|
val templateFile = fileOpener.openResourceFile("databases/template.db")
|
||||||
|
templateFile.copyTo(dbFile)
|
||||||
|
}
|
||||||
|
database = databaseOpener.open(dbFile)
|
||||||
|
database.migrateTo(LOOP_DATABASE_VERSION, fileOpener, log)
|
||||||
|
habitsRepository = HabitRepository(database)
|
||||||
|
checkmarkRepository = CheckmarkRepository(database, dateCalculator)
|
||||||
|
taskRunner.runInBackground {
|
||||||
|
habits.putAll(habitsRepository.findAll())
|
||||||
|
for ((key, habit) in habits) {
|
||||||
|
val checks = checkmarkRepository.findAll(key)
|
||||||
|
checkmarks[habit] = CheckmarkList(habit.frequency,
|
||||||
|
dateCalculator)
|
||||||
|
checkmarks[habit]?.setManualCheckmarks(checks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mainScreenDataSource = MainScreenDataSource(habits,
|
||||||
|
checkmarks,
|
||||||
|
taskRunner)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createHabit(habit: Habit) {
|
||||||
|
val id = habitsRepository.nextId()
|
||||||
|
habit.id = id
|
||||||
|
habit.position = habits.size
|
||||||
|
habits[id] = habit
|
||||||
|
checkmarks[habit] = CheckmarkList(habit.frequency, dateCalculator)
|
||||||
|
habitsRepository.insert(habit)
|
||||||
|
mainScreenDataSource.requestData()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteHabit(id: Int) {
|
||||||
|
habits[id]?.let { habit ->
|
||||||
|
habitsRepository.delete(habit)
|
||||||
|
habits.remove(id)
|
||||||
|
mainScreenDataSource.requestData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateHabit(modified: Habit) {
|
||||||
|
habits[modified.id]?.let { existing ->
|
||||||
|
modified.position = existing.position
|
||||||
|
habitsRepository.update(modified)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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.backend
|
||||||
|
|
||||||
|
import org.isoron.platform.concurrency.*
|
||||||
|
import org.isoron.platform.gui.*
|
||||||
|
import org.isoron.platform.time.*
|
||||||
|
import org.isoron.uhabits.models.*
|
||||||
|
|
||||||
|
class MainScreenDataSource(val habits: MutableMap<Int, Habit>,
|
||||||
|
val checkmarks: MutableMap<Habit, CheckmarkList>,
|
||||||
|
val taskRunner: TaskRunner) {
|
||||||
|
|
||||||
|
private val today = LocalDate(2019, 3, 30)
|
||||||
|
|
||||||
|
data class Data(val ids: List<Int>,
|
||||||
|
val scores: List<Double>,
|
||||||
|
val names: List<String>,
|
||||||
|
val colors: List<PaletteColor>,
|
||||||
|
val checkmarks: List<List<Int>>)
|
||||||
|
|
||||||
|
private val listeners = mutableListOf<Listener>()
|
||||||
|
|
||||||
|
fun addListener(listener: Listener) {
|
||||||
|
listeners.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeListener(listener: Listener) {
|
||||||
|
listeners.remove(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun onDataChanged(newData: Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestData() {
|
||||||
|
taskRunner.runInBackground {
|
||||||
|
val filteredHabits = habits.values.filter { h -> !h.isArchived }
|
||||||
|
val ids = filteredHabits.map { it.id }
|
||||||
|
val scores = filteredHabits.map { 0.0 }
|
||||||
|
val names = filteredHabits.map { it.name }
|
||||||
|
val colors = filteredHabits.map { it.color }
|
||||||
|
val ck = filteredHabits.map { habit ->
|
||||||
|
val allValues = checkmarks[habit]!!.getValuesUntil(today)
|
||||||
|
if (allValues.size <= 7) allValues
|
||||||
|
else allValues.subList(0, 7)
|
||||||
|
}
|
||||||
|
val data = Data(ids, scores, names, colors, ck)
|
||||||
|
taskRunner.runInForeground {
|
||||||
|
listeners.forEach { listener ->
|
||||||
|
listener.onDataChanged(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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.components
|
||||||
|
|
||||||
|
import org.isoron.platform.gui.*
|
||||||
|
import org.isoron.platform.time.*
|
||||||
|
import kotlin.math.*
|
||||||
|
|
||||||
|
class CalendarChart(var today: LocalDate,
|
||||||
|
var color: Color,
|
||||||
|
var theme: Theme,
|
||||||
|
var dateCalculator: LocalDateCalculator,
|
||||||
|
var dateFormatter: LocalDateFormatter) : Component {
|
||||||
|
|
||||||
|
var padding = 5.0
|
||||||
|
var backgroundColor = Color(0xFFFFFF)
|
||||||
|
var squareSpacing = 1.0
|
||||||
|
var series = listOf<Double>()
|
||||||
|
var scrollPosition = 0
|
||||||
|
|
||||||
|
private var squareSize = 0.0
|
||||||
|
private var fontSize = 0.0
|
||||||
|
|
||||||
|
override fun draw(canvas: Canvas) {
|
||||||
|
val width = canvas.getWidth()
|
||||||
|
val height = canvas.getHeight()
|
||||||
|
canvas.setColor(backgroundColor)
|
||||||
|
canvas.fillRect(0.0, 0.0, width, height)
|
||||||
|
squareSize = round((height - 2 * padding) / 8.0)
|
||||||
|
canvas.setFontSize(height * 0.06)
|
||||||
|
|
||||||
|
val nColumns = floor((width - 2 * padding) / squareSize).toInt() - 2
|
||||||
|
val todayWeekday = dateCalculator.dayOfWeek(today)
|
||||||
|
val topLeftOffset = (nColumns - 1 + scrollPosition) * 7 + todayWeekday.index
|
||||||
|
val topLeftDate = dateCalculator.minusDays(today, topLeftOffset)
|
||||||
|
|
||||||
|
repeat(nColumns) { column ->
|
||||||
|
val topOffset = topLeftOffset - 7 * column
|
||||||
|
val topDate = dateCalculator.plusDays(topLeftDate, 7 * column)
|
||||||
|
drawColumn(canvas, column, topDate, topOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.setColor(theme.mediumContrastTextColor)
|
||||||
|
repeat(7) { row ->
|
||||||
|
val date = dateCalculator.plusDays(topLeftDate, row)
|
||||||
|
canvas.setTextAlign(TextAlign.LEFT)
|
||||||
|
canvas.drawText(dateFormatter.shortWeekdayName(date),
|
||||||
|
padding + nColumns * squareSize + padding,
|
||||||
|
padding + squareSize * (row+1) + squareSize / 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawColumn(canvas: Canvas,
|
||||||
|
column: Int,
|
||||||
|
topDate: LocalDate,
|
||||||
|
topOffset: Int) {
|
||||||
|
drawHeader(canvas, column, topDate)
|
||||||
|
repeat(7) { row ->
|
||||||
|
val offset = topOffset - row
|
||||||
|
val date = dateCalculator.plusDays(topDate, row)
|
||||||
|
if (offset < 0) return
|
||||||
|
drawSquare(canvas,
|
||||||
|
padding + column * squareSize,
|
||||||
|
padding + (row + 1) * squareSize,
|
||||||
|
squareSize - squareSpacing,
|
||||||
|
squareSize - squareSpacing,
|
||||||
|
date,
|
||||||
|
offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawHeader(canvas: Canvas, column: Int, date: LocalDate) {
|
||||||
|
if (date.day >= 8) return
|
||||||
|
|
||||||
|
canvas.setColor(theme.mediumContrastTextColor)
|
||||||
|
if (date.month == 1) {
|
||||||
|
canvas.drawText(date.year.toString(),
|
||||||
|
padding + column * squareSize + squareSize / 2,
|
||||||
|
padding + squareSize / 2)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
canvas.drawText(dateFormatter.shortMonthName(date),
|
||||||
|
padding + column * squareSize + squareSize / 2,
|
||||||
|
padding + squareSize / 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawSquare(canvas: Canvas,
|
||||||
|
x: Double,
|
||||||
|
y: Double,
|
||||||
|
width: Double,
|
||||||
|
height: Double,
|
||||||
|
date: LocalDate,
|
||||||
|
offset: Int) {
|
||||||
|
|
||||||
|
var value = if (offset >= series.size) 0.0 else series[offset]
|
||||||
|
value = round(value * 5.0) / 5.0
|
||||||
|
|
||||||
|
var squareColor = color.blendWith(backgroundColor, 1 - value)
|
||||||
|
var textColor = backgroundColor
|
||||||
|
|
||||||
|
if (value == 0.0) squareColor = theme.lowContrastTextColor
|
||||||
|
if (squareColor.luminosity > 0.8)
|
||||||
|
textColor = squareColor.blendWith(theme.highContrastTextColor, 0.5)
|
||||||
|
|
||||||
|
canvas.setColor(squareColor)
|
||||||
|
canvas.fillRect(x, y, width, height)
|
||||||
|
canvas.setColor(textColor)
|
||||||
|
canvas.drawText(date.day.toString(), x + width / 2, y + width / 2)
|
||||||
|
}
|
||||||
|
}
|
@ -1,29 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
|
||||||
*
|
|
||||||
* 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.gui
|
|
||||||
|
|
||||||
data class PaletteColor(val index: Int)
|
|
||||||
|
|
||||||
class Color(val argb: Int) {
|
|
||||||
val alpha = 1.0
|
|
||||||
val red = ((argb shr 16) and 0xFF) / 255.0
|
|
||||||
val green = ((argb shr 8 ) and 0xFF) / 255.0
|
|
||||||
val blue = ((argb shr 0 ) and 0xFF) / 255.0
|
|
||||||
}
|
|
@ -0,0 +1,206 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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.models
|
||||||
|
|
||||||
|
import org.isoron.platform.time.*
|
||||||
|
import org.isoron.uhabits.models.Checkmark.Companion.CHECKED_AUTOMATIC
|
||||||
|
import org.isoron.uhabits.models.Checkmark.Companion.CHECKED_MANUAL
|
||||||
|
import org.isoron.uhabits.models.Checkmark.Companion.UNCHECKED
|
||||||
|
|
||||||
|
class CheckmarkList(private val frequency: Frequency,
|
||||||
|
private val dateCalculator: LocalDateCalculator) {
|
||||||
|
|
||||||
|
private val manualCheckmarks = mutableListOf<Checkmark>()
|
||||||
|
private val automaticCheckmarks = mutableListOf<Checkmark>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces the entire list of manual checkmarks by the ones provided. The
|
||||||
|
* list of automatic checkmarks will be automatically updated.
|
||||||
|
*/
|
||||||
|
fun setManualCheckmarks(checks: List<Checkmark>) {
|
||||||
|
manualCheckmarks.clear()
|
||||||
|
automaticCheckmarks.clear()
|
||||||
|
manualCheckmarks.addAll(checks)
|
||||||
|
automaticCheckmarks.addAll(computeAutomaticCheckmarks(checks,
|
||||||
|
frequency,
|
||||||
|
dateCalculator))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns values of all checkmarks (manual and automatic) from the oldest
|
||||||
|
* entry until the date provided.
|
||||||
|
*
|
||||||
|
* The interval is inclusive, and the list is sorted from newest to oldest.
|
||||||
|
* That is, the first element of the returned list corresponds to the date
|
||||||
|
* provided.
|
||||||
|
*/
|
||||||
|
fun getValuesUntil(date: LocalDate): List<Int> {
|
||||||
|
if (automaticCheckmarks.isEmpty()) return listOf()
|
||||||
|
|
||||||
|
val result = mutableListOf<Int>()
|
||||||
|
val newest = automaticCheckmarks.first().date
|
||||||
|
val distToNewest = dateCalculator.distanceInDays(newest, date)
|
||||||
|
|
||||||
|
var fromIndex = 0
|
||||||
|
val toIndex = automaticCheckmarks.size
|
||||||
|
if (newest.isOlderThan(date)) {
|
||||||
|
repeat(distToNewest) { result.add(UNCHECKED) }
|
||||||
|
} else {
|
||||||
|
fromIndex = distToNewest
|
||||||
|
}
|
||||||
|
val subList = automaticCheckmarks.subList(fromIndex, toIndex)
|
||||||
|
result.addAll(subList.map { it.value })
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Computes the list of automatic checkmarks a list of manual ones.
|
||||||
|
*/
|
||||||
|
fun computeAutomaticCheckmarks(checks: List<Checkmark>,
|
||||||
|
frequency: Frequency,
|
||||||
|
calc: LocalDateCalculator
|
||||||
|
): MutableList<Checkmark> {
|
||||||
|
|
||||||
|
val intervals = buildIntervals(checks, frequency, calc)
|
||||||
|
snapIntervalsTogether(intervals, calc)
|
||||||
|
return buildCheckmarksFromIntervals(checks, intervals, calc)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies the intervals so that gaps between intervals are eliminated.
|
||||||
|
*
|
||||||
|
* More specifically, this function shifts the beginning and the end of
|
||||||
|
* intervals so that they overlap the least as possible. The center of
|
||||||
|
* the interval, however, still falls within the interval. The length of
|
||||||
|
* the intervals are also not modified.
|
||||||
|
*/
|
||||||
|
fun snapIntervalsTogether(intervals: MutableList<Interval>,
|
||||||
|
calc: LocalDateCalculator) {
|
||||||
|
|
||||||
|
for (i in 1 until intervals.size) {
|
||||||
|
val (begin, center, end) = intervals[i]
|
||||||
|
val (_, _, prevEnd) = intervals[i - 1]
|
||||||
|
|
||||||
|
val gap = calc.distanceInDays(prevEnd, begin) - 1
|
||||||
|
val endMinusGap = calc.minusDays(end, gap)
|
||||||
|
if (gap <= 0 || endMinusGap.isOlderThan(center)) continue
|
||||||
|
intervals[i] = Interval(calc.minusDays(begin, gap),
|
||||||
|
center,
|
||||||
|
calc.minusDays(end, gap))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a list of (manually checked) checkmarks and computed
|
||||||
|
* intervals into a list of unchecked, manually checked and
|
||||||
|
* automatically checked checkmarks.
|
||||||
|
*
|
||||||
|
* Manual checkmarks are simply copied over to the output list. Days
|
||||||
|
* that are an interval, but which do not have manual checkmarks receive
|
||||||
|
* automatic checkmarks. Days that fall in the gaps between intervals
|
||||||
|
* receive unchecked checkmarks.
|
||||||
|
*/
|
||||||
|
fun buildCheckmarksFromIntervals(checks: List<Checkmark>,
|
||||||
|
intervals: List<Interval>,
|
||||||
|
calc: LocalDateCalculator
|
||||||
|
): MutableList<Checkmark> {
|
||||||
|
|
||||||
|
if (checks.isEmpty()) throw IllegalArgumentException()
|
||||||
|
if (intervals.isEmpty()) throw IllegalArgumentException()
|
||||||
|
|
||||||
|
var oldest = intervals[0].begin
|
||||||
|
var newest = intervals[0].end
|
||||||
|
for (interval in intervals) {
|
||||||
|
if (interval.begin.isOlderThan(oldest)) oldest = interval.begin
|
||||||
|
if (interval.end.isNewerThan(newest)) newest = interval.end
|
||||||
|
}
|
||||||
|
for (check in checks) {
|
||||||
|
if (check.date.isOlderThan(oldest)) oldest = check.date
|
||||||
|
if (check.date.isNewerThan(newest)) newest = check.date
|
||||||
|
}
|
||||||
|
|
||||||
|
val distance = calc.distanceInDays(oldest, newest)
|
||||||
|
val checkmarks = mutableListOf<Checkmark>()
|
||||||
|
for (offset in 0..distance)
|
||||||
|
checkmarks.add(Checkmark(calc.minusDays(newest, offset),
|
||||||
|
UNCHECKED))
|
||||||
|
|
||||||
|
for (interval in intervals) {
|
||||||
|
val beginOffset = calc.distanceInDays(newest, interval.begin)
|
||||||
|
val endOffset = calc.distanceInDays(newest, interval.end)
|
||||||
|
|
||||||
|
for (offset in endOffset..beginOffset) {
|
||||||
|
checkmarks.set(offset,
|
||||||
|
Checkmark(calc.minusDays(newest, offset),
|
||||||
|
CHECKED_AUTOMATIC))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (check in checks) {
|
||||||
|
val offset = calc.distanceInDays(newest, check.date)
|
||||||
|
checkmarks.set(offset, Checkmark(check.date, CHECKED_MANUAL))
|
||||||
|
}
|
||||||
|
|
||||||
|
return checkmarks
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a list of intervals based on a list of (manual)
|
||||||
|
* checkmarks.
|
||||||
|
*/
|
||||||
|
fun buildIntervals(checks: List<Checkmark>,
|
||||||
|
frequency: Frequency,
|
||||||
|
calc: LocalDateCalculator): MutableList<Interval> {
|
||||||
|
|
||||||
|
val num = frequency.numerator
|
||||||
|
val den = frequency.denominator
|
||||||
|
|
||||||
|
val intervals = mutableListOf<Interval>()
|
||||||
|
for (i in 0..(checks.size - num)) {
|
||||||
|
val first = checks[i]
|
||||||
|
val last = checks[i + num - 1]
|
||||||
|
|
||||||
|
val distance = calc.distanceInDays(first.date, last.date)
|
||||||
|
if (distance >= den) continue
|
||||||
|
|
||||||
|
val end = calc.plusDays(first.date, den - 1)
|
||||||
|
intervals.add(Interval(first.date, last.date, end))
|
||||||
|
}
|
||||||
|
|
||||||
|
return intervals
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* For non-daily habits, some groups of repetitions generate many
|
||||||
|
* automatic checkmarks. For weekly habits, each repetition generates
|
||||||
|
* seven checkmarks. For twice-a-week habits, two repetitions that are close
|
||||||
|
* enough together also generate seven checkmarks. This group of generated
|
||||||
|
* checkmarks is represented by an interval.
|
||||||
|
*
|
||||||
|
* The fields `begin` and `end` indicate the length of the interval, and are
|
||||||
|
* inclusive. The field `center` indicates the newest day within the interval
|
||||||
|
* that has a manual checkmark.
|
||||||
|
*/
|
||||||
|
data class Interval(val begin: LocalDate,
|
||||||
|
val center: LocalDate,
|
||||||
|
val end: LocalDate)
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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.models
|
||||||
|
|
||||||
|
import org.isoron.platform.io.*
|
||||||
|
import org.isoron.platform.time.*
|
||||||
|
|
||||||
|
class CheckmarkRepository(db: Database,
|
||||||
|
val dateCalculator: LocalDateCalculator) {
|
||||||
|
|
||||||
|
private val findStatement = db.prepareStatement("select timestamp, value from Repetitions where habit = ? order by timestamp desc")
|
||||||
|
private val insertStatement = db.prepareStatement("insert into Repetitions(habit, timestamp, value) values (?, ?, ?)")
|
||||||
|
private val deleteStatement = db.prepareStatement("delete from Repetitions where habit=? and timestamp=?")
|
||||||
|
|
||||||
|
fun findAll(habitId: Int): List<Checkmark> {
|
||||||
|
findStatement.bindInt(0, habitId)
|
||||||
|
val result = mutableListOf<Checkmark>()
|
||||||
|
while (findStatement.step() == StepResult.ROW) {
|
||||||
|
val timestamp = Timestamp(findStatement.getLong(0))
|
||||||
|
val value = findStatement.getInt(1)
|
||||||
|
val date = dateCalculator.fromTimestamp(timestamp)
|
||||||
|
result.add(Checkmark(date, value))
|
||||||
|
}
|
||||||
|
findStatement.reset()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun insert(habitId: Int, checkmark: Checkmark) {
|
||||||
|
val timestamp = dateCalculator.toTimestamp(checkmark.date)
|
||||||
|
insertStatement.bindInt(0, habitId)
|
||||||
|
insertStatement.bindLong(1, timestamp.unixTimeInMillis)
|
||||||
|
insertStatement.bindInt(2, checkmark.value)
|
||||||
|
insertStatement.step()
|
||||||
|
insertStatement.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun delete(habitId: Int, date: LocalDate) {
|
||||||
|
val timestamp = dateCalculator.toTimestamp(date)
|
||||||
|
deleteStatement.bindInt(0, habitId)
|
||||||
|
deleteStatement.bindLong(1, timestamp.unixTimeInMillis)
|
||||||
|
deleteStatement.step()
|
||||||
|
deleteStatement.reset()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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.platform.time
|
||||||
|
|
||||||
|
import java.lang.Math.*
|
||||||
|
import java.util.*
|
||||||
|
import java.util.Calendar.*
|
||||||
|
|
||||||
|
fun LocalDate.toGregorianCalendar(): GregorianCalendar {
|
||||||
|
val cal = GregorianCalendar(TimeZone.getTimeZone("GMT"))
|
||||||
|
cal.set(Calendar.HOUR_OF_DAY, 0)
|
||||||
|
cal.set(Calendar.MINUTE, 0)
|
||||||
|
cal.set(Calendar.SECOND, 0)
|
||||||
|
cal.set(Calendar.MILLISECOND, 0)
|
||||||
|
cal.set(Calendar.YEAR, this.year)
|
||||||
|
cal.set(Calendar.MONTH, this.month - 1)
|
||||||
|
cal.set(Calendar.DAY_OF_MONTH, this.day)
|
||||||
|
return cal
|
||||||
|
}
|
||||||
|
|
||||||
|
fun GregorianCalendar.toLocalDate(): LocalDate {
|
||||||
|
return LocalDate(this.get(YEAR),
|
||||||
|
this.get(MONTH) + 1,
|
||||||
|
this.get(DAY_OF_MONTH))
|
||||||
|
}
|
||||||
|
|
||||||
|
class JavaLocalDateFormatter(private val locale: Locale) : LocalDateFormatter {
|
||||||
|
override fun shortMonthName(date: LocalDate): String {
|
||||||
|
val cal = date.toGregorianCalendar()
|
||||||
|
val longName = cal.getDisplayName(MONTH, LONG, locale)
|
||||||
|
val shortName = cal.getDisplayName(MONTH, SHORT, locale)
|
||||||
|
|
||||||
|
// For some locales, such as Japan, SHORT name is exceedingly short
|
||||||
|
return if (longName.length <= 3) longName else shortName
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shortWeekdayName(date: LocalDate): String {
|
||||||
|
val cal = date.toGregorianCalendar()
|
||||||
|
return cal.getDisplayName(DAY_OF_WEEK, SHORT, locale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JavaLocalDateCalculator : LocalDateCalculator {
|
||||||
|
override fun toTimestamp(date: LocalDate): Timestamp {
|
||||||
|
val cal = date.toGregorianCalendar()
|
||||||
|
return Timestamp(cal.timeInMillis)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fromTimestamp(timestamp: Timestamp): LocalDate {
|
||||||
|
val cal = GregorianCalendar(TimeZone.getTimeZone("GMT"))
|
||||||
|
cal.timeInMillis = timestamp.unixTimeInMillis
|
||||||
|
return cal.toLocalDate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dayOfWeek(date: LocalDate): DayOfWeek {
|
||||||
|
val cal = date.toGregorianCalendar()
|
||||||
|
return when (cal.get(DAY_OF_WEEK)) {
|
||||||
|
Calendar.SATURDAY -> DayOfWeek.SATURDAY
|
||||||
|
Calendar.SUNDAY -> DayOfWeek.SUNDAY
|
||||||
|
Calendar.MONDAY -> DayOfWeek.MONDAY
|
||||||
|
Calendar.TUESDAY -> DayOfWeek.TUESDAY
|
||||||
|
Calendar.WEDNESDAY -> DayOfWeek.WEDNESDAY
|
||||||
|
Calendar.THURSDAY -> DayOfWeek.THURSDAY
|
||||||
|
else -> DayOfWeek.FRIDAY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun plusDays(date: LocalDate, days: Int): LocalDate {
|
||||||
|
val cal = date.toGregorianCalendar()
|
||||||
|
cal.add(Calendar.DAY_OF_MONTH, days)
|
||||||
|
return cal.toLocalDate()
|
||||||
|
}
|
||||||
|
}
|
@ -1,40 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
|
||||||
*
|
|
||||||
* 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.utils
|
|
||||||
|
|
||||||
import java.util.*
|
|
||||||
import java.util.Calendar.*
|
|
||||||
|
|
||||||
class JavaLocalDateFormatter(private val locale: Locale) : LocalDateFormatter {
|
|
||||||
override fun shortWeekdayName(date: LocalDate): String {
|
|
||||||
val d = GregorianCalendar(date.year, date.month - 1, date.day)
|
|
||||||
return d.getDisplayName(DAY_OF_WEEK, SHORT, locale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class JavaLocalDateCalculator : LocalDateCalculator {
|
|
||||||
override fun plusDays(date: LocalDate, days: Int): LocalDate {
|
|
||||||
val d = GregorianCalendar(date.year, date.month - 1, date.day)
|
|
||||||
d.add(Calendar.DAY_OF_MONTH, days)
|
|
||||||
return LocalDate(d.get(Calendar.YEAR),
|
|
||||||
d.get(Calendar.MONTH) + 1,
|
|
||||||
d.get(Calendar.DAY_OF_MONTH))
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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.platform
|
||||||
|
|
||||||
|
import junit.framework.TestCase.*
|
||||||
|
import org.isoron.platform.time.*
|
||||||
|
import org.junit.*
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
|
class JavaDatesTest {
|
||||||
|
private val calc = JavaLocalDateCalculator()
|
||||||
|
private val d1 = LocalDate(2019, 3, 25)
|
||||||
|
private val d2 = LocalDate(2019, 4, 4)
|
||||||
|
private val d3 = LocalDate(2019, 5, 12)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun plusMinusDays() {
|
||||||
|
val today = LocalDate(2019, 3, 25)
|
||||||
|
assertEquals(calc.minusDays(today, 28), LocalDate(2019, 2, 25))
|
||||||
|
assertEquals(calc.plusDays(today, 7), LocalDate(2019, 4, 1))
|
||||||
|
assertEquals(calc.plusDays(today, 42), LocalDate(2019, 5, 6))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shortMonthName() {
|
||||||
|
var fmt = JavaLocalDateFormatter(Locale.US)
|
||||||
|
assertEquals(fmt.shortWeekdayName(d1), "Mon")
|
||||||
|
assertEquals(fmt.shortWeekdayName(d2), "Thu")
|
||||||
|
assertEquals(fmt.shortWeekdayName(d3), "Sun")
|
||||||
|
assertEquals(fmt.shortMonthName(d1), "Mar")
|
||||||
|
assertEquals(fmt.shortMonthName(d2), "Apr")
|
||||||
|
assertEquals(fmt.shortMonthName(d3), "May")
|
||||||
|
|
||||||
|
fmt = JavaLocalDateFormatter(Locale.JAPAN)
|
||||||
|
assertEquals(fmt.shortWeekdayName(d1), "月")
|
||||||
|
assertEquals(fmt.shortWeekdayName(d2), "木")
|
||||||
|
assertEquals(fmt.shortWeekdayName(d3), "日")
|
||||||
|
assertEquals(fmt.shortMonthName(d1), "3月")
|
||||||
|
assertEquals(fmt.shortMonthName(d2), "4月")
|
||||||
|
assertEquals(fmt.shortMonthName(d3), "5月")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun weekDay() {
|
||||||
|
assertEquals(DayOfWeek.SUNDAY, calc.dayOfWeek(LocalDate(2015, 1, 25)))
|
||||||
|
assertEquals(DayOfWeek.MONDAY, calc.dayOfWeek(LocalDate(2017, 7, 3)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun timestamps() {
|
||||||
|
val timestamps = listOf(Timestamp(1555977600000),
|
||||||
|
Timestamp(968716800000),
|
||||||
|
Timestamp(0))
|
||||||
|
val dates = listOf(LocalDate(2019, 4, 23),
|
||||||
|
LocalDate(2000, 9, 12),
|
||||||
|
LocalDate(1970, 1, 1))
|
||||||
|
assertEquals(timestamps, dates.map { d -> calc.toTimestamp(d) })
|
||||||
|
assertEquals(dates, timestamps.map { t -> calc.fromTimestamp(t) })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun isOlderThan() {
|
||||||
|
val ref = LocalDate(2010, 10, 5)
|
||||||
|
assertTrue(ref.isOlderThan(LocalDate(2010, 10, 10)))
|
||||||
|
assertTrue(ref.isOlderThan(LocalDate(2010, 11, 4)))
|
||||||
|
assertTrue(ref.isOlderThan(LocalDate(2011, 1, 5)))
|
||||||
|
assertTrue(ref.isOlderThan(LocalDate(2015, 3, 1)))
|
||||||
|
|
||||||
|
assertFalse(ref.isOlderThan(LocalDate(2010, 10, 5)))
|
||||||
|
assertFalse(ref.isOlderThan(LocalDate(2010, 10, 4)))
|
||||||
|
assertFalse(ref.isOlderThan(LocalDate(2010, 9, 1)))
|
||||||
|
assertFalse(ref.isOlderThan(LocalDate(2005, 10, 5)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDistanceInDays() {
|
||||||
|
val d1 = LocalDate(2019, 5, 10)
|
||||||
|
val d2 = LocalDate(2019, 5, 30)
|
||||||
|
val d3 = LocalDate(2019, 6, 5)
|
||||||
|
|
||||||
|
assertEquals(0, calc.distanceInDays(d1, d1))
|
||||||
|
assertEquals(20, calc.distanceInDays(d1, d2))
|
||||||
|
assertEquals(20, calc.distanceInDays(d2, d1))
|
||||||
|
assertEquals(26, calc.distanceInDays(d1, d3))
|
||||||
|
assertEquals(6, calc.distanceInDays(d2, d3))
|
||||||
|
}
|
||||||
|
}
|
@ -1,48 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
|
|
||||||
import junit.framework.Assert.assertEquals
|
|
||||||
import junit.framework.Assert.assertTrue
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class BackendTest : BaseTest() {
|
|
||||||
@Test
|
|
||||||
fun testBackend() {
|
|
||||||
// val backend = Backend(databaseOpener, fileOpener)
|
|
||||||
// assertEquals(backend.getHabitList().size, 0)
|
|
||||||
//
|
|
||||||
// backend.createHabit("Brush teeth")
|
|
||||||
// backend.createHabit("Wake up early")
|
|
||||||
|
|
||||||
// var result = backend.getHabitList()
|
|
||||||
// assertEquals(2, result.size)
|
|
||||||
// assertEquals(result[0]["name"], "Brush teeth")
|
|
||||||
// assertEquals(result[0]["name"], "Wake up early")
|
|
||||||
//
|
|
||||||
// backend.deleteHabit(1)
|
|
||||||
// result = backend.getHabitList()
|
|
||||||
// assertEquals(result.size, 1)
|
|
||||||
//
|
|
||||||
// backend.updateHabit(2, "Wake up late")
|
|
||||||
// result = backend.getHabitList()
|
|
||||||
// assertEquals(result[2]["name"], "Wake up late")
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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.backend
|
||||||
|
|
||||||
|
import junit.framework.TestCase.*
|
||||||
|
import org.isoron.platform.gui.*
|
||||||
|
import org.isoron.uhabits.*
|
||||||
|
import org.junit.*
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.*
|
||||||
|
|
||||||
|
class BackendTest : BaseTest() {
|
||||||
|
lateinit var backend: Backend
|
||||||
|
private val latch = CountDownLatch(1)
|
||||||
|
val dbFilename = "uhabits${Random().nextInt()}.db"
|
||||||
|
val dbFile = fileOpener.openUserFile(dbFilename)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
override fun setUp() {
|
||||||
|
super.setUp()
|
||||||
|
if (dbFile.exists()) dbFile.delete()
|
||||||
|
backend = Backend(dbFilename,
|
||||||
|
databaseOpener,
|
||||||
|
fileOpener,
|
||||||
|
log,
|
||||||
|
dateCalculator,
|
||||||
|
taskRunner)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
dbFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMainScreenDataSource() {
|
||||||
|
val listener = object : MainScreenDataSource.Listener {
|
||||||
|
override fun onDataChanged(newData: MainScreenDataSource.Data) {
|
||||||
|
val expected = MainScreenDataSource.Data(
|
||||||
|
ids = listOf(0, 10, 9, 2, 3, 4, 5, 11, 6, 7, 8),
|
||||||
|
scores = listOf(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||||
|
0.0, 0.0, 0.0),
|
||||||
|
names = listOf("Wake up early", "Eat healthy", "Floss",
|
||||||
|
"Journal", "Track time", "Meditate",
|
||||||
|
"Work out", "Take a walk", "Read books",
|
||||||
|
"Learn French", "Play chess"),
|
||||||
|
colors = listOf(PaletteColor(8), PaletteColor(8),
|
||||||
|
PaletteColor(8), PaletteColor(11),
|
||||||
|
PaletteColor(11), PaletteColor(15),
|
||||||
|
PaletteColor(15), PaletteColor(15),
|
||||||
|
PaletteColor(2), PaletteColor(2),
|
||||||
|
PaletteColor(13)),
|
||||||
|
checkmarks = listOf(
|
||||||
|
listOf(2, 0, 0, 0, 0, 2, 0),
|
||||||
|
listOf(0, 2, 2, 2, 2, 2, 0),
|
||||||
|
listOf(0, 0, 0, 0, 2, 0, 0),
|
||||||
|
listOf(0, 2, 0, 2, 0, 0, 0),
|
||||||
|
listOf(2, 2, 2, 0, 2, 2, 2),
|
||||||
|
listOf(2, 1, 1, 2, 1, 2, 2),
|
||||||
|
listOf(2, 0, 2, 0, 2, 1, 2),
|
||||||
|
listOf(0, 2, 2, 2, 2, 0, 0),
|
||||||
|
listOf(0, 2, 2, 2, 2, 2, 0),
|
||||||
|
listOf(0, 0, 2, 0, 2, 0, 2),
|
||||||
|
listOf(0, 2, 0, 0, 2, 2, 0)))
|
||||||
|
assertEquals(newData, expected)
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
backend.mainScreenDataSource.addListener(listener)
|
||||||
|
backend.mainScreenDataSource.requestData()
|
||||||
|
assertTrue(latch.await(3, TimeUnit.SECONDS))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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.components
|
||||||
|
|
||||||
|
import org.isoron.platform.time.*
|
||||||
|
import org.isoron.uhabits.*
|
||||||
|
import org.junit.*
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class CalendarChartTest : BaseViewTest() {
|
||||||
|
val base = "components/CalendarChart"
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDraw() {
|
||||||
|
val component = CalendarChart(LocalDate(2015, 1, 25),
|
||||||
|
theme.color(4),
|
||||||
|
theme,
|
||||||
|
JavaLocalDateCalculator(),
|
||||||
|
JavaLocalDateFormatter(Locale.US))
|
||||||
|
component.series = listOf(1.0, // today
|
||||||
|
0.2, 0.5, 0.7, 0.0, 0.3, 0.4, 0.6,
|
||||||
|
0.6, 0.0, 0.3, 0.6, 0.5, 0.8, 0.0,
|
||||||
|
0.0, 0.0, 0.0, 0.6, 0.5, 0.7, 0.7,
|
||||||
|
0.5, 0.5, 0.8, 0.9, 1.0, 1.0, 1.0,
|
||||||
|
1.0, 1.0, 1.0, 1.0, 1.0, 0.5, 0.2)
|
||||||
|
assertRenders(800, 400, "$base/base.png", component)
|
||||||
|
|
||||||
|
component.scrollPosition = 2
|
||||||
|
assertRenders(800, 400, "$base/scroll.png", component)
|
||||||
|
|
||||||
|
component.dateFormatter = JavaLocalDateFormatter(Locale.JAPAN)
|
||||||
|
assertRenders(800, 400, "$base/base-jp.png", component)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,195 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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.models
|
||||||
|
|
||||||
|
import org.isoron.platform.time.*
|
||||||
|
import org.isoron.uhabits.*
|
||||||
|
import org.isoron.uhabits.models.Checkmark.Companion.CHECKED_AUTOMATIC
|
||||||
|
import org.isoron.uhabits.models.Checkmark.Companion.CHECKED_MANUAL
|
||||||
|
import org.isoron.uhabits.models.Checkmark.Companion.UNCHECKED
|
||||||
|
import org.junit.Test
|
||||||
|
import kotlin.test.*
|
||||||
|
|
||||||
|
class CheckmarkListTest : BaseTest() {
|
||||||
|
|
||||||
|
private val today = LocalDate(2019, 1, 30)
|
||||||
|
|
||||||
|
private fun day(offset: Int): LocalDate {
|
||||||
|
return dateCalculator.minusDays(today, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun buildIntervalsWeekly() {
|
||||||
|
val checks = listOf(Checkmark(day(23), CHECKED_MANUAL),
|
||||||
|
Checkmark(day(18), CHECKED_MANUAL),
|
||||||
|
Checkmark(day(8), CHECKED_MANUAL))
|
||||||
|
val expected = listOf(
|
||||||
|
CheckmarkList.Interval(day(23), day(23), day(17)),
|
||||||
|
CheckmarkList.Interval(day(18), day(18), day(12)),
|
||||||
|
CheckmarkList.Interval(day(8), day(8), day(2)))
|
||||||
|
val actual = CheckmarkList.buildIntervals(checks,
|
||||||
|
Frequency.WEEKLY,
|
||||||
|
dateCalculator)
|
||||||
|
assertEquals(expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun buildIntervalsDaily() {
|
||||||
|
val checks = listOf(Checkmark(day(23), CHECKED_MANUAL),
|
||||||
|
Checkmark(day(18), CHECKED_MANUAL),
|
||||||
|
Checkmark(day(8), CHECKED_MANUAL))
|
||||||
|
val expected = listOf(
|
||||||
|
CheckmarkList.Interval(day(23), day(23), day(23)),
|
||||||
|
CheckmarkList.Interval(day(18), day(18), day(18)),
|
||||||
|
CheckmarkList.Interval(day(8), day(8), day(8)))
|
||||||
|
val actual = CheckmarkList.buildIntervals(checks,
|
||||||
|
Frequency.DAILY,
|
||||||
|
dateCalculator)
|
||||||
|
assertEquals(expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun buildIntervalsTwoPerWeek() {
|
||||||
|
val checks = listOf(Checkmark(day(23), CHECKED_MANUAL),
|
||||||
|
Checkmark(day(22), CHECKED_MANUAL),
|
||||||
|
Checkmark(day(18), CHECKED_MANUAL),
|
||||||
|
Checkmark(day(15), CHECKED_MANUAL),
|
||||||
|
Checkmark(day(8), CHECKED_MANUAL))
|
||||||
|
val expected = listOf(
|
||||||
|
CheckmarkList.Interval(day(23), day(22), day(17)),
|
||||||
|
CheckmarkList.Interval(day(22), day(18), day(16)),
|
||||||
|
CheckmarkList.Interval(day(18), day(15), day(12)))
|
||||||
|
val actual = CheckmarkList.buildIntervals(checks,
|
||||||
|
Frequency.TWO_TIMES_PER_WEEK,
|
||||||
|
dateCalculator)
|
||||||
|
assertEquals(expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSnapIntervalsTogether() {
|
||||||
|
val original = mutableListOf(
|
||||||
|
CheckmarkList.Interval(day(40), day(40), day(34)),
|
||||||
|
CheckmarkList.Interval(day(25), day(25), day(19)),
|
||||||
|
CheckmarkList.Interval(day(16), day(16), day(10)),
|
||||||
|
CheckmarkList.Interval(day(8), day(8), day(2)))
|
||||||
|
val expected = listOf(
|
||||||
|
CheckmarkList.Interval(day(40), day(40), day(34)),
|
||||||
|
CheckmarkList.Interval(day(25), day(25), day(19)),
|
||||||
|
CheckmarkList.Interval(day(18), day(16), day(12)),
|
||||||
|
CheckmarkList.Interval(day(11), day(8), day(5)))
|
||||||
|
CheckmarkList.snapIntervalsTogether(original, dateCalculator)
|
||||||
|
assertEquals(expected, original)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testBuildCheckmarksFromIntervals() {
|
||||||
|
val checks = listOf(Checkmark(day(10), CHECKED_MANUAL),
|
||||||
|
Checkmark(day(5), CHECKED_MANUAL),
|
||||||
|
Checkmark(day(2), CHECKED_MANUAL),
|
||||||
|
Checkmark(day(1), CHECKED_MANUAL))
|
||||||
|
val intervals = listOf(CheckmarkList.Interval(day(10), day(8), day(8)),
|
||||||
|
CheckmarkList.Interval(day(6), day(5), day(4)),
|
||||||
|
CheckmarkList.Interval(day(2), day(2), day(1)))
|
||||||
|
val expected = listOf(Checkmark(day(1), CHECKED_MANUAL),
|
||||||
|
Checkmark(day(2), CHECKED_MANUAL),
|
||||||
|
Checkmark(day(3), UNCHECKED),
|
||||||
|
Checkmark(day(4), CHECKED_AUTOMATIC),
|
||||||
|
Checkmark(day(5), CHECKED_MANUAL),
|
||||||
|
Checkmark(day(6), CHECKED_AUTOMATIC),
|
||||||
|
Checkmark(day(7), UNCHECKED),
|
||||||
|
Checkmark(day(8), CHECKED_AUTOMATIC),
|
||||||
|
Checkmark(day(9), CHECKED_AUTOMATIC),
|
||||||
|
Checkmark(day(10), CHECKED_MANUAL))
|
||||||
|
val actual = CheckmarkList.buildCheckmarksFromIntervals(checks,
|
||||||
|
intervals,
|
||||||
|
dateCalculator)
|
||||||
|
assertEquals(expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testBuildCheckmarksFromIntervals2() {
|
||||||
|
val reps = listOf(Checkmark(day(0), CHECKED_MANUAL))
|
||||||
|
val intervals = listOf(CheckmarkList.Interval(day(5), day(0), day(0)))
|
||||||
|
val expected = listOf(Checkmark(day(0), CHECKED_MANUAL),
|
||||||
|
Checkmark(day(1), CHECKED_AUTOMATIC),
|
||||||
|
Checkmark(day(2), CHECKED_AUTOMATIC),
|
||||||
|
Checkmark(day(3), CHECKED_AUTOMATIC),
|
||||||
|
Checkmark(day(4), CHECKED_AUTOMATIC),
|
||||||
|
Checkmark(day(5), CHECKED_AUTOMATIC))
|
||||||
|
val actual = CheckmarkList.buildCheckmarksFromIntervals(reps,
|
||||||
|
intervals,
|
||||||
|
dateCalculator)
|
||||||
|
assertEquals(expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun computeAutomaticCheckmarks() {
|
||||||
|
val checks = listOf(Checkmark(day(10), CHECKED_MANUAL),
|
||||||
|
Checkmark(day(5), CHECKED_MANUAL),
|
||||||
|
Checkmark(day(2), CHECKED_MANUAL),
|
||||||
|
Checkmark(day(1), CHECKED_MANUAL))
|
||||||
|
val expected = listOf(Checkmark(day(-1), CHECKED_AUTOMATIC),
|
||||||
|
Checkmark(day(0), CHECKED_AUTOMATIC),
|
||||||
|
Checkmark(day(1), CHECKED_MANUAL),
|
||||||
|
Checkmark(day(2), CHECKED_MANUAL),
|
||||||
|
Checkmark(day(3), CHECKED_AUTOMATIC),
|
||||||
|
Checkmark(day(4), CHECKED_AUTOMATIC),
|
||||||
|
Checkmark(day(5), CHECKED_MANUAL),
|
||||||
|
Checkmark(day(6), CHECKED_AUTOMATIC),
|
||||||
|
Checkmark(day(7), CHECKED_AUTOMATIC),
|
||||||
|
Checkmark(day(8), CHECKED_AUTOMATIC),
|
||||||
|
Checkmark(day(9), CHECKED_AUTOMATIC),
|
||||||
|
Checkmark(day(10), CHECKED_MANUAL))
|
||||||
|
val actual = CheckmarkList.computeAutomaticCheckmarks(checks,
|
||||||
|
Frequency(1, 3),
|
||||||
|
dateCalculator)
|
||||||
|
assertEquals(expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testGetValuesUntil() {
|
||||||
|
val list = CheckmarkList(Frequency(1, 2), dateCalculator)
|
||||||
|
list.setManualCheckmarks(listOf(Checkmark(day(4), CHECKED_MANUAL),
|
||||||
|
Checkmark(day(7), CHECKED_MANUAL)))
|
||||||
|
val expected = listOf(UNCHECKED,
|
||||||
|
UNCHECKED,
|
||||||
|
UNCHECKED,
|
||||||
|
CHECKED_AUTOMATIC,
|
||||||
|
CHECKED_MANUAL,
|
||||||
|
UNCHECKED,
|
||||||
|
CHECKED_AUTOMATIC,
|
||||||
|
CHECKED_MANUAL)
|
||||||
|
assertEquals(expected, list.getValuesUntil(day(0)))
|
||||||
|
|
||||||
|
val expected2 = listOf(CHECKED_AUTOMATIC,
|
||||||
|
CHECKED_MANUAL,
|
||||||
|
UNCHECKED,
|
||||||
|
CHECKED_AUTOMATIC,
|
||||||
|
CHECKED_MANUAL)
|
||||||
|
assertEquals(expected2, list.getValuesUntil(day(3)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testGetValuesUntil2() {
|
||||||
|
val list = CheckmarkList(Frequency(1, 2), dateCalculator)
|
||||||
|
val expected = listOf<Int>()
|
||||||
|
assertEquals(expected, list.getValuesUntil(day(0)))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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.models
|
||||||
|
|
||||||
|
import junit.framework.TestCase.*
|
||||||
|
import org.isoron.platform.time.*
|
||||||
|
import org.isoron.uhabits.*
|
||||||
|
import org.junit.*
|
||||||
|
|
||||||
|
class CheckmarkRepositoryTest : BaseTest() {
|
||||||
|
@Test
|
||||||
|
fun testCRUD() {
|
||||||
|
val habitA = 10
|
||||||
|
var checkmarksA = listOf(Checkmark(LocalDate(2019, 1, 15), 100),
|
||||||
|
Checkmark(LocalDate(2019, 1, 7), 500),
|
||||||
|
Checkmark(LocalDate(2019, 1, 1), 900))
|
||||||
|
|
||||||
|
val habitB = 35
|
||||||
|
val checkmarksB = listOf(Checkmark(LocalDate(2019, 1, 30), 50),
|
||||||
|
Checkmark(LocalDate(2019, 1, 29), 30),
|
||||||
|
Checkmark(LocalDate(2019, 1, 27), 900),
|
||||||
|
Checkmark(LocalDate(2019, 1, 25), 450),
|
||||||
|
Checkmark(LocalDate(2019, 1, 20), 1000))
|
||||||
|
|
||||||
|
val repository = CheckmarkRepository(db, JavaLocalDateCalculator())
|
||||||
|
|
||||||
|
for (c in checkmarksA) repository.insert(habitA, c)
|
||||||
|
for (c in checkmarksB) repository.insert(habitB, c)
|
||||||
|
assertEquals(checkmarksA, repository.findAll(habitA))
|
||||||
|
assertEquals(checkmarksB, repository.findAll(habitB))
|
||||||
|
assertEquals(listOf<Checkmark>(), repository.findAll(999))
|
||||||
|
|
||||||
|
checkmarksA = listOf(Checkmark(LocalDate(2019, 1, 15), 100),
|
||||||
|
Checkmark(LocalDate(2019, 1, 1), 900))
|
||||||
|
repository.delete(habitA, LocalDate(2019, 1, 7))
|
||||||
|
assertEquals(checkmarksA, repository.findAll(habitA))
|
||||||
|
}
|
||||||
|
}
|
@ -1,50 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
|
||||||
*
|
|
||||||
* 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.utils
|
|
||||||
|
|
||||||
import junit.framework.TestCase.*
|
|
||||||
import org.junit.*
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
|
|
||||||
class JavaDatesTest {
|
|
||||||
val calc = JavaLocalDateCalculator()
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun plusMinusDays() {
|
|
||||||
val today = LocalDate(2019, 3, 25)
|
|
||||||
assertEquals(calc.minusDays(today, 28), LocalDate(2019, 2, 25))
|
|
||||||
assertEquals(calc.plusDays(today, 7), LocalDate(2019, 4, 1))
|
|
||||||
assertEquals(calc.plusDays(today, 42), LocalDate(2019, 5, 6))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shortMonthName() {
|
|
||||||
var fmt = JavaLocalDateFormatter(Locale.US)
|
|
||||||
assertEquals(fmt.shortWeekdayName(LocalDate(2019, 3, 25)), "Mon")
|
|
||||||
assertEquals(fmt.shortWeekdayName(LocalDate(2019, 4, 4)), "Thu")
|
|
||||||
assertEquals(fmt.shortWeekdayName(LocalDate(2019, 5, 12)), "Sun")
|
|
||||||
|
|
||||||
fmt = JavaLocalDateFormatter(Locale.JAPAN)
|
|
||||||
assertEquals(fmt.shortWeekdayName(LocalDate(2019, 3, 25)), "月")
|
|
||||||
assertEquals(fmt.shortWeekdayName(LocalDate(2019, 4, 4)), "木")
|
|
||||||
assertEquals(fmt.shortWeekdayName(LocalDate(2019, 5, 12)), "日")
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class ShowHabitController : UITableViewController {
|
||||||
|
|
||||||
|
let theme: Theme
|
||||||
|
let color: Color
|
||||||
|
var cells = [UITableViewCell]()
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
init(theme: Theme, color: Color) {
|
||||||
|
self.theme = theme
|
||||||
|
self.color = color
|
||||||
|
super.init(style: .grouped)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
self.title = "Exercise"
|
||||||
|
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit,
|
||||||
|
target: self,
|
||||||
|
action: #selector(self.onEditHabitClicked))
|
||||||
|
cells.append(buildHistoryChartCell())
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildHistoryChartCell() -> UITableViewCell {
|
||||||
|
let component = CalendarChart(today: LocalDate(year: 2019, month: 3, day: 15),
|
||||||
|
color: color,
|
||||||
|
theme: theme,
|
||||||
|
dateCalculator: IosLocalDateCalculator(),
|
||||||
|
dateFormatter: IosLocalDateFormatter())
|
||||||
|
let cell = UITableViewCell()
|
||||||
|
let view = ComponentView(frame: cell.frame, component: component)
|
||||||
|
var series = [KotlinDouble]()
|
||||||
|
for _ in 1...365 {
|
||||||
|
series.append(KotlinDouble(value: Double.random(in: 0...1)))
|
||||||
|
}
|
||||||
|
component.series = series
|
||||||
|
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
|
cell.contentView.addSubview(view)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
self.navigationController?.navigationBar.barStyle = .blackOpaque
|
||||||
|
self.navigationController?.navigationBar.barTintColor = color.uicolor
|
||||||
|
self.navigationController?.navigationBar.tintColor = .white
|
||||||
|
self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||||
|
return cells.count
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
return cells[indexPath.section]
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func onEditHabitClicked() {
|
||||||
|
self.navigationController?.pushViewController(EditHabitController(), animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||||
|
return 200
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +0,0 @@
|
|||||||
create table Habits ( id integer primary key autoincrement, archived integer, color integer, description text, freq_den integer, freq_num integer, highlight integer, name text, position integer, reminder_hour integer, reminder_min integer )
|
|
||||||
create table Checkmarks ( id integer primary key autoincrement, habit integer references habits(id), timestamp integer, value integer )
|
|
||||||
create table Repetitions ( id integer primary key autoincrement, habit integer references habits(id), timestamp integer )
|
|
||||||
create table Streak ( id integer primary key autoincrement, end integer, habit integer references habits(id), length integer, start integer )
|
|
||||||
create table Score ( id integer primary key autoincrement, habit integer references habits(id), score integer, timestamp integer )
|
|
@ -1,3 +0,0 @@
|
|||||||
delete from Score
|
|
||||||
delete from Streak
|
|
||||||
delete from Checkmarks
|
|
@ -1 +0,0 @@
|
|||||||
alter table Habits add column reminder_days integer not null default 127
|
|
@ -1,3 +0,0 @@
|
|||||||
delete from Score
|
|
||||||
delete from Streak
|
|
||||||
delete from Checkmarks
|
|
@ -1,4 +0,0 @@
|
|||||||
create index idx_score_habit_timestamp on Score(habit, timestamp)
|
|
||||||
create index idx_checkmark_habit_timestamp on Checkmarks(habit, timestamp)
|
|
||||||
create index idx_repetitions_habit_timestamp on Repetitions(habit, timestamp)
|
|
||||||
create index idx_streak_habit_end on Streak(habit, end)
|
|
@ -1,14 +0,0 @@
|
|||||||
update habits set color=0 where color=-2937041
|
|
||||||
update habits set color=1 where color=-1684967
|
|
||||||
update habits set color=2 where color=-415707
|
|
||||||
update habits set color=3 where color=-5262293
|
|
||||||
update habits set color=4 where color=-13070788
|
|
||||||
update habits set color=5 where color=-16742021
|
|
||||||
update habits set color=6 where color=-16732991
|
|
||||||
update habits set color=7 where color=-16540699
|
|
||||||
update habits set color=8 where color=-10603087
|
|
||||||
update habits set color=9 where color=-7461718
|
|
||||||
update habits set color=10 where color=-2614432
|
|
||||||
update habits set color=11 where color=-13619152
|
|
||||||
update habits set color=12 where color=-5592406
|
|
||||||
update habits set color=0 where color<0 or color>12
|
|
@ -1,3 +0,0 @@
|
|||||||
delete from Score
|
|
||||||
delete from Streak
|
|
||||||
delete from Checkmarks
|
|
@ -1,2 +0,0 @@
|
|||||||
alter table Habits add column type integer not null default 0
|
|
||||||
alter table Repetitions add column value integer not null default 2
|
|
@ -1,5 +0,0 @@
|
|||||||
drop table Score
|
|
||||||
create table Score ( id integer primary key autoincrement, habit integer references habits(id), score real, timestamp integer)
|
|
||||||
create index idx_score_habit_timestamp on Score(habit, timestamp)
|
|
||||||
delete from streak
|
|
||||||
delete from checkmarks
|
|
@ -1,3 +0,0 @@
|
|||||||
alter table Habits add column target_type integer not null default 0
|
|
||||||
alter table Habits add column target_value real not null default 0
|
|
||||||
alter table Habits add column unit text not null default ""
|
|
@ -1 +0,0 @@
|
|||||||
create table Events ( id integer primary key autoincrement, timestamp integer, message text, server_id integer )
|
|
@ -1,3 +0,0 @@
|
|||||||
drop table checkmarks
|
|
||||||
drop table streak
|
|
||||||
drop table score
|
|
@ -1,12 +0,0 @@
|
|||||||
update habits set color=19 where color=12
|
|
||||||
update habits set color=17 where color=11
|
|
||||||
update habits set color=15 where color=10
|
|
||||||
update habits set color=14 where color=9
|
|
||||||
update habits set color=13 where color=8
|
|
||||||
update habits set color=10 where color=7
|
|
||||||
update habits set color=9 where color=6
|
|
||||||
update habits set color=8 where color=5
|
|
||||||
update habits set color=7 where color=4
|
|
||||||
update habits set color=5 where color=3
|
|
||||||
update habits set color=4 where color=2
|
|
||||||
update habits set color=0 where color<0 or color>19
|
|
@ -1,11 +0,0 @@
|
|||||||
delete from repetitions where habit not in (select id from habits)
|
|
||||||
delete from repetitions where timestamp is null
|
|
||||||
delete from repetitions where habit is null
|
|
||||||
delete from repetitions where rowid not in ( select min(rowid) from repetitions group by habit, timestamp )
|
|
||||||
alter table Repetitions rename to RepetitionsBak
|
|
||||||
create table Repetitions ( id integer primary key autoincrement, habit integer not null references habits(id), timestamp integer not null, value integer not null)
|
|
||||||
drop index if exists idx_repetitions_habit_timestamp
|
|
||||||
create unique index idx_repetitions_habit_timestamp on Repetitions( habit, timestamp)
|
|
||||||
insert into Repetitions select * from RepetitionsBak
|
|
||||||
drop table RepetitionsBak
|
|
||||||
pragma foreign_keys=ON
|
|