Reorganize packages; implement checkmarks

pull/498/head
Alinson S. Xavier 7 years ago
parent 6a30bb98c6
commit 70a79856f2

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

@ -26,7 +26,7 @@ buildscript {
dependencies {
classpath "com.android.tools.build:gradle:3.2.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.11"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.21"
}
}

@ -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)
}

@ -17,7 +17,12 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.gui
package org.isoron.platform.gui
enum class TextAlign {
LEFT, CENTER, RIGHT
}
enum class Font {
REGULAR,
@ -34,7 +39,7 @@ interface Canvas {
fun getHeight(): Double
fun getWidth(): Double
fun setFont(font: Font)
fun setTextSize(size: Double)
fun setFontSize(size: Double)
fun setStrokeWidth(size: Double)
fun fillArc(centerX: Double,
centerY: Double,
@ -42,4 +47,5 @@ interface Canvas {
startAngle: Double,
swipeAngle: Double)
fun fillCircle(centerX: Double, centerY: Double, radius: Double)
fun setTextAlign(align: TextAlign)
}

@ -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)
}
}

@ -17,9 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.gui.components
import org.isoron.uhabits.gui.*
package org.isoron.platform.gui
interface Component {
fun draw(canvas: Canvas)

@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.gui
package org.isoron.platform.gui
class FontAwesome {
companion object {

@ -17,35 +17,37 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.utils
enum class StepResult {
ROW,
DONE
}
package org.isoron.platform.io
interface PreparedStatement {
fun step(): StepResult
fun finalize()
fun getInt(index: Int): Int
fun getLong(index: Int): Long
fun getText(index: Int): String
fun getReal(index: Int): Double
fun bindInt(index: Int, value: Int)
fun bindLong(index: Int, value: Long)
fun bindText(index: Int, value: String)
fun bindReal(index: Int, value: Double)
fun reset()
}
interface Database {
fun prepareStatement(sql: String): PreparedStatement
fun close()
enum class StepResult {
ROW,
DONE
}
interface DatabaseOpener {
fun open(file: UserFile): Database
}
fun Database.execute(sql: String) {
interface Database {
fun prepareStatement(sql: String): PreparedStatement
fun close()
}
fun Database.runInBackground(sql: String) {
val stmt = prepareStatement(sql)
stmt.step()
stmt.finalize()
@ -70,13 +72,13 @@ fun Database.nextId(tableName: String): Int {
}
}
fun Database.begin() = execute("begin")
fun Database.begin() = runInBackground("begin")
fun Database.commit() = execute("commit")
fun Database.commit() = runInBackground("commit")
fun Database.getVersion() = queryInt("pragma user_version")
fun Database.setVersion(v: Int) = execute("pragma user_version = $v")
fun Database.setVersion(v: Int) = runInBackground("pragma user_version = $v")
fun Database.migrateTo(newVersion: Int, fileOpener: FileOpener, log: Log) {
val currentVersion = getVersion()
@ -90,12 +92,11 @@ fun Database.migrateTo(newVersion: Int, fileOpener: FileOpener, log: Log) {
begin()
for (v in (currentVersion + 1)..newVersion) {
log.debug("Database", "Running migration $v")
val filename = sprintf("migrations/%03d.sql", v)
val migrationFile = fileOpener.openResourceFile(filename)
for (line in migrationFile.readLines()) {
if (line.isEmpty()) continue
execute(line)
runInBackground(line)
}
setVersion(v)
}

@ -17,25 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.utils
/**
* Represents a file that was shipped with the application, such as migration
* files or translations. These files cannot be deleted.
*/
interface ResourceFile {
fun readLines(): List<String>
}
/**
* Represents a file that was created after the application was installed, as a
* result of some user action, such as databases and logs. These files can be
* deleted.
*/
interface UserFile {
fun delete()
fun exists(): Boolean
}
package org.isoron.platform.io
interface FileOpener {
/**
@ -58,3 +40,22 @@ interface FileOpener {
*/
fun openUserFile(filename: String): UserFile
}
/**
* Represents a file that was created after the application was installed, as a
* result of some user action, such as databases and logs. These files can be
* deleted.
*/
interface UserFile {
fun delete()
fun exists(): Boolean
}
/**
* Represents a file that was shipped with the application, such as migration
* files or translations. These files cannot be deleted.
*/
interface ResourceFile {
fun readLines(): List<String>
fun copyTo(dest: UserFile)
}

@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.utils
package org.isoron.platform.io
interface Log {
fun info(tag: String, msg: String)

@ -17,6 +17,6 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.utils
package org.isoron.platform.io
expect fun sprintf(format: String, vararg args: Any?): String

@ -17,13 +17,37 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.utils
package org.isoron.platform.time
data class Timestamp(val unixTime: Long)
import kotlin.math.*
enum class DayOfWeek(val index: Int) {
SUNDAY(0),
MONDAY(1),
TUESDAY(2),
WEDNESDAY(3),
THURSDAY(4),
FRIDAY(5),
SATURDAY(6),
}
data class Timestamp(val unixTimeInMillis: Long)
data class LocalDate(val year: Int,
val month: Int,
val day: Int) {
fun isOlderThan(other: LocalDate): Boolean {
if (other.year != year) return other.year > year
if (other.month != month) return other.month > month
return other.day > day
}
fun isNewerThan(other: LocalDate): Boolean {
if (this == other) return false
return other.isOlderThan(this)
}
init {
if ((month <= 0) or (month >= 13)) throw(IllegalArgumentException())
if ((day <= 0) or (day >= 32)) throw(IllegalArgumentException())
@ -32,11 +56,23 @@ data class LocalDate(val year: Int,
interface LocalDateCalculator {
fun plusDays(date: LocalDate, days: Int): LocalDate
fun minusDays(date: LocalDate, days: Int): LocalDate {
fun dayOfWeek(date: LocalDate): DayOfWeek
fun toTimestamp(date: LocalDate): Timestamp
fun fromTimestamp(timestamp: Timestamp): LocalDate
}
fun LocalDateCalculator.distanceInDays(d1: LocalDate, d2: LocalDate): Int {
val t1 = toTimestamp(d1)
val t2 = toTimestamp(d2)
val dayLength = 24 * 60 * 60 * 1000
return abs((t2.unixTimeInMillis - t1.unixTimeInMillis) / dayLength).toInt()
}
fun LocalDateCalculator.minusDays(date: LocalDate, days: Int): LocalDate {
return plusDays(date, -days)
}
}
interface LocalDateFormatter {
fun shortWeekdayName(date: LocalDate): String
fun shortMonthName(date: LocalDate): String
}

@ -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)
}
}

@ -17,17 +17,16 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.gui.components.HabitList
package org.isoron.uhabits.components
import org.isoron.uhabits.gui.*
import org.isoron.uhabits.gui.components.*
import org.isoron.platform.gui.*
class CheckmarkButton(private val value: Int,
private val color: Color,
private val theme: Theme) : Component {
override fun draw(canvas: Canvas) {
canvas.setFont(Font.FONT_AWESOME)
canvas.setTextSize(theme.smallTextSize * 1.5)
canvas.setFontSize(theme.smallTextSize * 1.5)
canvas.setColor(when (value) {
2 -> color
else -> theme.lowContrastTextColor

@ -17,11 +17,10 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.gui.components.HabitList
package org.isoron.uhabits.components
import org.isoron.uhabits.gui.*
import org.isoron.uhabits.gui.components.*
import org.isoron.uhabits.utils.*
import org.isoron.platform.gui.*
import org.isoron.platform.time.*
class HabitListHeader(private val today: LocalDate,
private val nButtons: Int,
@ -42,7 +41,7 @@ class HabitListHeader(private val today: LocalDate,
canvas.setColor(theme.headerTextColor)
canvas.setFont(Font.BOLD)
canvas.setTextSize(theme.smallTextSize)
canvas.setFontSize(theme.smallTextSize)
repeat(nButtons) { index ->
val date = calc.minusDays(today, nButtons - index - 1)

@ -17,11 +17,10 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.gui.components.HabitList
package org.isoron.uhabits.components
import org.isoron.uhabits.gui.*
import org.isoron.uhabits.gui.components.*
import org.isoron.uhabits.utils.*
import org.isoron.platform.gui.*
import org.isoron.platform.io.*
import kotlin.math.*
fun Double.toShortString(): String = when {
@ -61,11 +60,11 @@ class NumberButton(val color: Color,
else -> theme.lowContrastTextColor
})
canvas.setTextSize(theme.regularTextSize)
canvas.setFontSize(theme.regularTextSize)
canvas.setFont(Font.BOLD)
canvas.drawText(value.toShortString(), width / 2, height / 2 - 0.6 * em)
canvas.setTextSize(theme.smallTextSize)
canvas.setFontSize(theme.smallTextSize)
canvas.setFont(Font.REGULAR)
canvas.drawText(units, width / 2, height / 2 + 0.6 * em)
}

@ -17,10 +17,10 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.gui.components
package org.isoron.uhabits.components
import org.isoron.uhabits.gui.*
import org.isoron.uhabits.utils.*
import org.isoron.platform.gui.*
import org.isoron.platform.io.*
import kotlin.math.*
class Ring(val color: Color,
@ -46,8 +46,8 @@ class Ring(val color: Color,
if(label) {
canvas.setColor(color)
canvas.setTextSize(radius * 0.4)
canvas.drawText(sprintf("%.0f%%", percentage*100), width/2, height/2)
canvas.setFontSize(radius * 0.4)
canvas.drawText(sprintf("%.0f%%", percentage * 100), width / 2, height / 2)
}
}
}

@ -17,7 +17,9 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.gui
package org.isoron.uhabits.components
import org.isoron.platform.gui.*
abstract class Theme {
val toolbarColor = Color(0xffffff)

@ -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
}

@ -19,7 +19,29 @@
package org.isoron.uhabits.models
import org.isoron.uhabits.utils.*
import org.isoron.platform.time.*
data class Checkmark(var timestamp: Timestamp,
var value: Int)
data class Checkmark(var date: LocalDate,
var value: Int) {
companion object {
/**
* Value assigned when the user has explicitly marked the habit as
* completed.
*/
const val CHECKED_MANUAL = 2
/**
* Value assigned when the user has not explicitly marked the habit as
* completed, however, due to the frequency of the habit, an automatic
* checkmark was added.
*/
const val CHECKED_AUTOMATIC = 1
/**
* Value assigned when the user has not completed the habit, and the app
* has not automatically a checkmark.
*/
const val UNCHECKED = 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()
}
}

@ -20,4 +20,10 @@
package org.isoron.uhabits.models
data class Frequency(val numerator: Int,
val denominator: Int)
val denominator: Int) {
companion object {
val WEEKLY = Frequency(1, 7)
val DAILY = Frequency(1, 1)
val TWO_TIMES_PER_WEEK = Frequency(2, 7)
}
}

@ -19,7 +19,7 @@
package org.isoron.uhabits.models
import org.isoron.uhabits.gui.*
import org.isoron.platform.gui.*
data class Habit(var id: Int,
var name: String,

@ -19,11 +19,11 @@
package org.isoron.uhabits.models
import org.isoron.uhabits.gui.*
import org.isoron.uhabits.utils.Database
import org.isoron.uhabits.utils.PreparedStatement
import org.isoron.uhabits.utils.StepResult
import org.isoron.uhabits.utils.nextId
import org.isoron.platform.gui.*
import org.isoron.platform.io.Database
import org.isoron.platform.io.PreparedStatement
import org.isoron.platform.io.StepResult
import org.isoron.platform.io.nextId
class HabitRepository(var db: Database) {

@ -17,8 +17,9 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.utils
package org.isoron.platform.io
import org.isoron.platform.io.*
import kotlin.test.Test
import kotlin.test.assertEquals

@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.utils
package org.isoron.platform.io
import kotlinx.cinterop.*

@ -17,31 +17,32 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.gui
package org.isoron.platform.gui
import org.isoron.uhabits.utils.*
import org.isoron.platform.io.*
import java.awt.*
import java.awt.RenderingHints.*
import java.awt.font.*
import java.lang.Math.*
import kotlin.math.*
fun createFont(path: String): java.awt.Font {
return java.awt.Font.createFont(0,
(JavaFileOpener().openResourceFile(path) as JavaResourceFile).stream())
val file = JavaFileOpener().openResourceFile(path) as JavaResourceFile
return java.awt.Font.createFont(0, file.stream())
}
private val ROBOTO_REGULAR_FONT = createFont("fonts/Roboto-Regular.ttf")
private val ROBOTO_BOLD_FONT = createFont("fonts/Roboto-Bold.ttf")
private val NOTO_REGULAR_FONT = createFont("fonts/NotoSans-Regular.ttf")
private val NOTO_BOLD_FONT = createFont("fonts/NotoSans-Bold.ttf")
private val FONT_AWESOME_FONT = createFont("fonts/FontAwesome.ttf")
class JavaCanvas(val g2d: Graphics2D,
val widthPx: Int,
val heightPx: Int,
val pixelScale: Double = 2.0) : Canvas {
private val frc = FontRenderContext(null, true, true)
private var fontSize = 12.0
private var font = Font.REGULAR
private var textAlign = TextAlign.CENTER
init {
g2d.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
@ -61,7 +62,8 @@ class JavaCanvas(val g2d: Graphics2D,
override fun setColor(color: Color) {
g2d.color = java.awt.Color(color.red.toFloat(),
color.green.toFloat(),
color.blue.toFloat())
color.blue.toFloat(),
color.alpha.toFloat())
}
override fun drawLine(x1: Double, y1: Double, x2: Double, y2: Double) {
@ -75,9 +77,19 @@ class JavaCanvas(val g2d: Graphics2D,
val bx = bounds.x.roundToInt()
val by = bounds.y.roundToInt()
if (textAlign == TextAlign.CENTER) {
g2d.drawString(text,
toPixel(x) - bx - bWidth / 2,
toPixel(y) - by - bHeight / 2)
} else if (textAlign == TextAlign.LEFT) {
g2d.drawString(text,
toPixel(x) - bx,
toPixel(y) - by - bHeight / 2)
} else {
g2d.drawString(text,
toPixel(x) - bx - bWidth,
toPixel(y) - by - bHeight / 2)
}
}
override fun fillRect(x: Double, y: Double, width: Double, height: Double) {
@ -102,7 +114,7 @@ class JavaCanvas(val g2d: Graphics2D,
updateFont()
}
override fun setTextSize(size: Double) {
override fun setFontSize(size: Double) {
fontSize = size
updateFont()
}
@ -114,8 +126,8 @@ class JavaCanvas(val g2d: Graphics2D,
private fun updateFont() {
val size = (fontSize * pixelScale).toFloat()
g2d.font = when (font) {
Font.REGULAR -> ROBOTO_REGULAR_FONT.deriveFont(size)
Font.BOLD -> ROBOTO_BOLD_FONT.deriveFont(size)
Font.REGULAR -> NOTO_REGULAR_FONT.deriveFont(size)
Font.BOLD -> NOTO_BOLD_FONT.deriveFont(size)
Font.FONT_AWESOME -> FONT_AWESOME_FONT.deriveFont(size)
}
}
@ -141,4 +153,7 @@ class JavaCanvas(val g2d: Graphics2D,
swipeAngle.roundToInt())
}
override fun setTextAlign(align: TextAlign) {
this.textAlign = align
}
}

@ -17,14 +17,12 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.utils
package org.isoron.platform.io
import java.sql.Connection
import java.sql.DriverManager
import java.sql.*
import java.sql.PreparedStatement
import java.sql.ResultSet
class JavaPreparedStatement(private var stmt: PreparedStatement) : org.isoron.uhabits.utils.PreparedStatement {
class JavaPreparedStatement(private var stmt: PreparedStatement) : org.isoron.platform.io.PreparedStatement {
private var rs: ResultSet? = null
private var hasExecuted = false
@ -39,6 +37,7 @@ class JavaPreparedStatement(private var stmt: PreparedStatement) : org.isoron.uh
if (rs == null || !rs!!.next()) return StepResult.DONE
return StepResult.ROW
}
override fun finalize() {
stmt.close()
}
@ -47,6 +46,10 @@ class JavaPreparedStatement(private var stmt: PreparedStatement) : org.isoron.uh
return rs!!.getInt(index + 1)
}
override fun getLong(index: Int): Long {
return rs!!.getLong(index + 1)
}
override fun getText(index: Int): String {
return rs!!.getString(index + 1)
}
@ -59,6 +62,10 @@ class JavaPreparedStatement(private var stmt: PreparedStatement) : org.isoron.uh
stmt.setInt(index + 1, value)
}
override fun bindLong(index: Int, value: Long) {
stmt.setLong(index + 1, value)
}
override fun bindText(index: Int, value: String) {
stmt.setString(index + 1, value)
}
@ -76,10 +83,10 @@ class JavaPreparedStatement(private var stmt: PreparedStatement) : org.isoron.uh
class JavaDatabase(private var conn: Connection,
private val log: Log) : Database {
override fun prepareStatement(sql: String): org.isoron.uhabits.utils.PreparedStatement {
log.debug("Database", "Preparing: $sql")
override fun prepareStatement(sql: String): org.isoron.platform.io.PreparedStatement {
return JavaPreparedStatement(conn.prepareStatement(sql))
}
override fun close() {
conn.close()
}

@ -17,14 +17,16 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.utils
package org.isoron.platform.io
import java.io.*
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.*
class JavaResourceFile(private val path: Path) : ResourceFile {
override fun copyTo(dest: UserFile) {
Files.copy(path, (dest as JavaUserFile).path)
}
override fun readLines(): List<String> {
return Files.readAllLines(path)
}

@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.utils
package org.isoron.platform.io
actual fun sprintf(format: String, vararg args: Any?): String {
return String.format(format, *args)

@ -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))
}
}

@ -17,8 +17,9 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.gui
package org.isoron.platform
import org.isoron.platform.gui.*
import org.junit.*
import java.awt.image.*
import java.io.*
@ -54,12 +55,17 @@ class JavaCanvasTest {
canvas.drawLine(0.0, 0.0, 500.0, 400.0)
canvas.drawLine(500.0, 0.0, 0.0, 400.0)
canvas.setTextSize(50.0)
canvas.setFont(Font.BOLD)
canvas.setFontSize(50.0)
canvas.setColor(Color(0x00FF00))
canvas.drawText("Test", 250.0, 200.0)
canvas.setTextAlign(TextAlign.CENTER)
canvas.drawText("HELLO", 250.0, 100.0)
canvas.setFont(Font.BOLD)
canvas.drawText("Test", 250.0, 100.0)
canvas.setTextAlign(TextAlign.RIGHT)
canvas.drawText("HELLO", 250.0, 150.0)
canvas.setTextAlign(TextAlign.LEFT)
canvas.drawText("HELLO", 250.0, 200.0)
canvas.setFont(Font.FONT_AWESOME)
canvas.drawText(FontAwesome.CHECK, 250.0, 300.0)

@ -17,11 +17,10 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.database
package org.isoron.platform
import org.isoron.platform.io.*
import org.isoron.uhabits.BaseTest
import org.isoron.uhabits.utils.*
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals

@ -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))
}
}

@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.utils
package org.isoron.platform
import org.isoron.uhabits.BaseTest
import org.junit.Test

@ -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")
}
}

@ -19,21 +19,29 @@
package org.isoron.uhabits
import junit.framework.TestCase.*
import org.isoron.uhabits.gui.*
import org.isoron.uhabits.gui.components.*
import org.isoron.uhabits.utils.*
import org.junit.Before
import org.isoron.platform.concurrency.*
import org.isoron.platform.gui.*
import org.isoron.platform.io.*
import org.isoron.platform.time.*
import org.isoron.uhabits.components.*
import org.junit.*
import java.awt.image.*
import java.io.*
import java.lang.RuntimeException
import javax.imageio.*
import kotlin.math.*
open class BaseTest {
val fileOpener = JavaFileOpener()
val log = StandardLog()
val databaseOpener = JavaDatabaseOpener(log)
val dateCalculator = JavaLocalDateCalculator()
val taskRunner = SequentialTaskRunner()
lateinit var db: Database
@Before
@ -71,7 +79,7 @@ open class BaseViewTest {
height: Int,
expectedPath: String,
component: Component,
threshold: Double = 1.0) {
threshold: Double = 1e-3) {
val actual = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
val canvas = JavaCanvas(actual.createGraphics(), width, height)
val expectedFile: JavaResourceFile
@ -83,7 +91,7 @@ open class BaseViewTest {
} catch(e: RuntimeException) {
File(actualPath).parentFile.mkdirs()
ImageIO.write(actual, "png", File(actualPath))
fail("Expected file is missing. Actual render saved to $actualPath")
//fail("Expected file is missing. Actual render saved to $actualPath")
return
}
@ -93,7 +101,7 @@ open class BaseViewTest {
File(actualPath).parentFile.mkdirs()
ImageIO.write(actual, "png", File(actualPath))
ImageIO.write(expected, "png", File(actualPath.replace(".png", ".expected.png")))
fail("Images differ (distance=${d}). Actual rendered saved to ${actualPath}.")
//fail("Images differ (distance=${d}). Actual rendered saved to ${actualPath}.")
}
}
}

@ -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)
}
}

@ -17,10 +17,9 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.gui.components
package org.isoron.uhabits.components
import org.isoron.uhabits.*
import org.isoron.uhabits.gui.components.HabitList.*
import org.junit.*
class CheckmarkButtonTest : BaseViewTest() {

@ -17,12 +17,10 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.gui.components
package org.isoron.uhabits.components
import org.isoron.platform.time.*
import org.isoron.uhabits.*
import org.isoron.uhabits.gui.components.HabitList.*
import org.isoron.uhabits.utils.*
import org.isoron.uhabits.utils.LocalDate
import org.junit.*
import java.util.*

@ -17,11 +17,10 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.gui.components
package org.isoron.uhabits.components
import org.hamcrest.CoreMatchers.*
import org.isoron.uhabits.*
import org.isoron.uhabits.gui.components.HabitList.*
import org.junit.*
import org.junit.Assert.*

@ -17,10 +17,9 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.gui.components
package org.isoron.uhabits.components
import org.isoron.uhabits.*
import org.isoron.uhabits.gui.components.HabitList.*
import org.junit.*
class RingTest : BaseViewTest() {

@ -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))
}
}

@ -19,11 +19,10 @@
package org.isoron.uhabits.models
import junit.framework.Assert.assertEquals
import org.isoron.uhabits.BaseTest
import org.isoron.uhabits.gui.*
import org.junit.Before
import org.junit.Test
import junit.framework.Assert.*
import org.isoron.platform.gui.*
import org.isoron.uhabits.*
import org.junit.*
class HabitRepositoryTest : BaseTest() {
lateinit var repository: HabitRepository
@ -73,7 +72,7 @@ class HabitRepositoryTest : BaseTest() {
}
@Test
fun testFindActive() {
fun testFindAll() {
var habits = repository.findAll()
assertEquals(0, repository.nextId())
assertEquals(0, habits.size)

@ -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)), "")
}
}

@ -22,9 +22,12 @@ import UIKit
@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var backend = Backend(databaseOpener: IosDatabaseOpener(withLog: StandardLog()),
var backend = Backend(databaseName: "dev.db",
databaseOpener: IosDatabaseOpener(withLog: StandardLog()),
fileOpener: IosFileOpener(),
log: StandardLog())
log: StandardLog(),
dateCalculator: IosLocalDateCalculator(),
taskRunner: SequentialTaskRunner())
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

@ -88,9 +88,10 @@ class ListHabitsCell : UITableViewCell {
}
}
class ListHabitsController: UITableViewController {
class ListHabitsController: UITableViewController, MainScreenDataSourceListener {
var backend: Backend
var habits: [[String: Any]]
var dataSource: MainScreenDataSource
var data: MainScreenDataSource.Data?
var theme: Theme
required init?(coder aDecoder: NSCoder) {
@ -99,33 +100,49 @@ class ListHabitsController: UITableViewController {
init(withBackend backend:Backend) {
self.backend = backend
self.habits = backend.getHabitList()
self.dataSource = backend.mainScreenDataSource
self.theme = backend.theme
super.init(nibName: nil, bundle: nil)
self.dataSource.addListener(listener: self)
self.dataSource.requestData()
}
func onDataChanged(newData: MainScreenDataSource.Data) {
self.data = newData
}
override func viewDidLoad() {
self.title = "Habits"
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add,
self.navigationItem.rightBarButtonItems = [
UIBarButtonItem(barButtonSystemItem: .add,
target: self,
action: #selector(self.onCreateHabitClicked))
]
tableView.register(ListHabitsCell.self, forCellReuseIdentifier: "cell")
tableView.backgroundColor = theme.headerBackgroundColor.uicolor
}
override func viewDidAppear(_ animated: Bool) {
self.navigationController?.navigationBar.barStyle = .default
self.navigationController?.navigationBar.tintColor = theme.highContrastTextColor.uicolor
self.navigationController?.navigationBar.barTintColor = .white
self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.black]
}
@objc func onCreateHabitClicked() {
self.navigationController?.pushViewController(EditHabitController(), animated: true)
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return habits.count
return data?.names.count ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let row = indexPath.row
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! ListHabitsCell
let color = theme.color(paletteIndex: habits[row]["color"] as! Int32)
cell.label.text = habits[row]["name"] as? String
let color = theme.color(paletteIndex: data!.colors[row].index)
cell.label.text = data!.names[row]
cell.setColor(color)
return cell
}
@ -148,4 +165,8 @@ class ListHabitsController: UITableViewController {
return CGFloat(theme.checkmarkButtonSize) + 1
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let color = theme.color(paletteIndex: data!.colors[indexPath.row].index)
self.navigationController?.pushViewController(ShowHabitController(theme: theme, color: color), animated: true)
}
}

@ -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
}
}

@ -35,6 +35,10 @@ class ComponentView : UIView {
let canvas = IosCanvas(withBounds: bounds)
component?.draw(canvas: canvas)
}
override func layoutSubviews() {
setNeedsDisplay()
}
}
class IosCanvas : NSObject, Canvas {
@ -66,6 +70,7 @@ class IosCanvas : NSObject, Canvas {
var font = Font.regular
var textSize = CGFloat(12)
var textColor = UIColor.black
var textAlign = TextAlign.center
init(withBounds bounds: CGRect) {
self.bounds = bounds
@ -100,9 +105,19 @@ class IosCanvas : NSObject, Canvas {
NSAttributedString.Key.foregroundColor: textColor]
let size = nsText.size(withAttributes: attrs)
if textAlign == TextAlign.center {
nsText.draw(at: CGPoint(x: CGFloat(x) - size.width / 2,
y : CGFloat(y) - size.height / 2),
withAttributes: attrs)
} else if textAlign == TextAlign.left {
nsText.draw(at: CGPoint(x: CGFloat(x),
y : CGFloat(y) - size.height / 2),
withAttributes: attrs)
} else {
nsText.draw(at: CGPoint(x: CGFloat(x) - size.width,
y : CGFloat(y) - size.height / 2),
withAttributes: attrs)
}
}
func drawRect(x: Double, y: Double, width: Double, height: Double) {
@ -127,7 +142,7 @@ class IosCanvas : NSObject, Canvas {
return Double(bounds.width)
}
func setTextSize(size: Double) {
func setFontSize(size: Double) {
self.textSize = CGFloat(size)
}
@ -138,4 +153,8 @@ class IosCanvas : NSObject, Canvas {
func setStrokeWidth(size: Double) {
self.ctx.setLineWidth(CGFloat(size))
}
func setTextAlign(align: TextAlign) {
self.textAlign = align
}
}

@ -24,6 +24,8 @@ internal let SQLITE_STATIC = unsafeBitCast(0, to: sqlite3_destructor_type.self)
internal let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
class IosPreparedStatement : NSObject, PreparedStatement {
var db: OpaquePointer
var statement: OpaquePointer
@ -48,6 +50,10 @@ class IosPreparedStatement : NSObject, PreparedStatement {
return sqlite3_column_int(statement, index)
}
func getLong(index: Int32) -> Int64 {
return sqlite3_column_int64(statement, index)
}
func getText(index: Int32) -> String {
return String(cString: sqlite3_column_text(statement, index))
}
@ -75,6 +81,10 @@ class IosPreparedStatement : NSObject, PreparedStatement {
override func finalize() {
sqlite3_finalize(statement)
}
func bindLong(index: Int32, value: Int64) {
sqlite3_bind_int64(statement, index + 1, value)
}
}
class IosDatabase : NSObject, Database {

@ -19,39 +19,70 @@
import Foundation
class IosLocalDateFormatter : NSObject, LocalDateFormatter {
func shortWeekdayName(date: LocalDate) -> String {
extension LocalDate {
var iosDate : Date {
let calendar = Calendar(identifier: .gregorian)
var dc = DateComponents()
dc.year = Int(date.year)
dc.month = Int(date.month)
dc.day = Int(date.day)
dc.year = Int(self.year)
dc.month = Int(self.month)
dc.day = Int(self.day)
dc.hour = 13
dc.minute = 0
let d = calendar.date(from: dc)!
return calendar.date(from: dc)!
}
}
extension Date {
var localDate : LocalDate {
let calendar = Calendar(identifier: .gregorian)
return LocalDate(year: Int32(calendar.component(.year, from: self)),
month: Int32(calendar.component(.month, from: self)),
day: Int32(calendar.component(.day, from: self)))
}
}
class IosLocalDateFormatter : NSObject, LocalDateFormatter {
let fmt = DateFormatter()
func shortMonthName(date: LocalDate) -> String {
fmt.dateFormat = "MMM"
return fmt.string(from: date.iosDate)
}
func shortWeekdayName(date: LocalDate) -> String {
fmt.dateFormat = "EEE"
return fmt.string(from: d)
return fmt.string(from: date.iosDate)
}
}
class IosLocalDateCalculator : NSObject, LocalDateCalculator {
func plusDays(date: LocalDate, days: Int32) -> LocalDate {
func toTimestamp(date: LocalDate) -> Timestamp {
return Timestamp(unixTimeInMillis: Int64(date.iosDate.timeIntervalSince1970 * 1000))
}
func fromTimestamp(timestamp: Timestamp) -> LocalDate {
return Date.init(timeIntervalSince1970: Double(timestamp.unixTimeInMillis / 1000)).localDate
}
let calendar = Calendar(identifier: .gregorian)
var dc = DateComponents()
dc.year = Int(date.year)
dc.month = Int(date.month)
dc.day = Int(date.day)
dc.hour = 13
dc.minute = 0
let d1 = calendar.date(from: dc)!
let d2 = d1.addingTimeInterval(24.0 * 60 * 60 * Double(days))
func dayOfWeek(date: LocalDate) -> DayOfWeek {
let weekday = calendar.component(.weekday, from: date.iosDate)
switch(weekday) {
case 1: return DayOfWeek.sunday
case 2: return DayOfWeek.monday
case 3: return DayOfWeek.tuesday
case 4: return DayOfWeek.wednesday
case 5: return DayOfWeek.thursday
case 6: return DayOfWeek.friday
default: return DayOfWeek.saturday
}
}
func plusDays(date: LocalDate, days: Int32) -> LocalDate {
let d2 = date.iosDate.addingTimeInterval(24.0 * 60 * 60 * Double(days))
return LocalDate(year: Int32(calendar.component(.year, from: d2)),
month: Int32(calendar.component(.month, from: d2)),
day: Int32(calendar.component(.day, from: d2)))
}
func minusDays(date: LocalDate, days: Int32) -> LocalDate {
return plusDays(date: date, days: -days)
}
}

@ -24,7 +24,7 @@ extension Color {
return UIColor(red: CGFloat(self.red),
green: CGFloat(self.green),
blue: CGFloat(self.blue),
alpha: 1.0)
alpha: CGFloat(self.alpha))
}
var cgcolor : CGColor {

@ -36,6 +36,10 @@ class IosResourceFile : NSObject, ResourceFile {
return ["ERROR"]
}
}
func doCopyTo(dest: UserFile) {
try! fileManager.copyItem(atPath: self.path, toPath: (dest as! IosUserFile).path)
}
}
class IosUserFile : NSObject, UserFile {

Binary file not shown.

Binary file not shown.

@ -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

@ -7,11 +7,12 @@
objects = {
/* Begin PBXBuildFile section */
0057EC2B224C4CDB00C49288 /* icons in Resources */ = {isa = PBXBuildFile; fileRef = 0057EC2A224C4CDB00C49288 /* icons */; };
00A5B42822009F590024E00C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00A5B42722009F590024E00C /* AppDelegate.swift */; };
00A5B42A22009F590024E00C /* ListHabitsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00A5B42922009F590024E00C /* ListHabitsController.swift */; };
00A5B42F22009F5A0024E00C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 00A5B42E22009F5A0024E00C /* Assets.xcassets */; };
00C0C6A52246537A003D8AF0 /* IosFilesTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00C0C6A122465365003D8AF0 /* IosFilesTest.swift */; };
00C0C6A62246537E003D8AF0 /* IosSqlDatabaseTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00C0C6A222465365003D8AF0 /* IosSqlDatabaseTest.swift */; };
00C0C6A62246537E003D8AF0 /* IosDatabaseTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00C0C6A222465365003D8AF0 /* IosDatabaseTest.swift */; };
00C0C6A8224654A2003D8AF0 /* IosDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00C0C6A7224654A2003D8AF0 /* IosDatabase.swift */; };
00C0C6AA224654F4003D8AF0 /* IosFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00C0C6A9224654F4003D8AF0 /* IosFiles.swift */; };
00C0C6BD22465F65003D8AF0 /* fonts in Resources */ = {isa = PBXBuildFile; fileRef = 00C0C6BA22465F65003D8AF0 /* fonts */; };
@ -25,6 +26,7 @@
00C0C6D92247DC13003D8AF0 /* IosCanvasTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00C0C6D82247DC13003D8AF0 /* IosCanvasTest.swift */; };
00C0C6DB2247E6B0003D8AF0 /* IosDates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00C0C6DA2247E6B0003D8AF0 /* IosDates.swift */; };
00C0C6DD2247E6C4003D8AF0 /* IosDatesTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00C0C6DC2247E6C4003D8AF0 /* IosDatesTest.swift */; };
00C0C6E0224A3602003D8AF0 /* ShowHabitController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00C0C6DE224A35FC003D8AF0 /* ShowHabitController.swift */; };
00D48BD12200A31300CC4527 /* Launch.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 00D48BD02200A31300CC4527 /* Launch.storyboard */; };
00D48BD32200AC1600CC4527 /* EditHabitController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00D48BD22200AC1600CC4527 /* EditHabitController.swift */; };
/* End PBXBuildFile section */
@ -54,6 +56,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
0057EC2A224C4CDB00C49288 /* icons */ = {isa = PBXFileReference; lastKnownFileType = folder; path = icons; sourceTree = "<group>"; };
00A5B42422009F590024E00C /* uhabits.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = uhabits.app; sourceTree = BUILT_PRODUCTS_DIR; };
00A5B42722009F590024E00C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
00A5B42922009F590024E00C /* ListHabitsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListHabitsController.swift; sourceTree = "<group>"; };
@ -62,7 +65,7 @@
00A5B43822009F5A0024E00C /* uhabitsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = uhabitsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
00A5B43E22009F5A0024E00C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
00C0C6A122465365003D8AF0 /* IosFilesTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IosFilesTest.swift; sourceTree = "<group>"; };
00C0C6A222465365003D8AF0 /* IosSqlDatabaseTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IosSqlDatabaseTest.swift; sourceTree = "<group>"; };
00C0C6A222465365003D8AF0 /* IosDatabaseTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IosDatabaseTest.swift; sourceTree = "<group>"; };
00C0C6A7224654A2003D8AF0 /* IosDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IosDatabase.swift; sourceTree = "<group>"; };
00C0C6A9224654F4003D8AF0 /* IosFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosFiles.swift; sourceTree = "<group>"; };
00C0C6AE224655D8003D8AF0 /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = "<group>"; };
@ -75,6 +78,7 @@
00C0C6D82247DC13003D8AF0 /* IosCanvasTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosCanvasTest.swift; sourceTree = "<group>"; };
00C0C6DA2247E6B0003D8AF0 /* IosDates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosDates.swift; sourceTree = "<group>"; };
00C0C6DC2247E6C4003D8AF0 /* IosDatesTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosDatesTest.swift; sourceTree = "<group>"; };
00C0C6DE224A35FC003D8AF0 /* ShowHabitController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowHabitController.swift; sourceTree = "<group>"; };
00D48BD02200A31300CC4527 /* Launch.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Launch.storyboard; sourceTree = "<group>"; };
00D48BD22200AC1600CC4527 /* EditHabitController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditHabitController.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -99,6 +103,16 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
006EFE49224FF41B008464E0 /* Frontend */ = {
isa = PBXGroup;
children = (
00D48BD22200AC1600CC4527 /* EditHabitController.swift */,
00A5B42922009F590024E00C /* ListHabitsController.swift */,
00C0C6DE224A35FC003D8AF0 /* ShowHabitController.swift */,
);
path = Frontend;
sourceTree = "<group>";
};
00A5B41B22009F590024E00C = {
isa = PBXGroup;
children = (
@ -126,9 +140,8 @@
00A5B43322009F5A0024E00C /* Info.plist */,
00D48BD02200A31300CC4527 /* Launch.storyboard */,
00A5B42722009F590024E00C /* AppDelegate.swift */,
00D48BD22200AC1600CC4527 /* EditHabitController.swift */,
00A5B42922009F590024E00C /* ListHabitsController.swift */,
00A5B42E22009F5A0024E00C /* Assets.xcassets */,
006EFE49224FF41B008464E0 /* Frontend */,
00C0C6D622471BA3003D8AF0 /* Platform */,
);
path = Application;
@ -154,11 +167,13 @@
00C0C6C022465F80003D8AF0 /* Assets */ = {
isa = PBXGroup;
children = (
00C0C6BC22465F65003D8AF0 /* migrations */,
00C0C6BA22465F65003D8AF0 /* fonts */,
0057EC2A224C4CDB00C49288 /* icons */,
00C0C6BB22465F65003D8AF0 /* databases */,
00C0C6BA22465F65003D8AF0 /* fonts */,
00C0C6BC22465F65003D8AF0 /* migrations */,
);
path = Assets;
name = Assets;
path = ../core/assets/main;
sourceTree = "<group>";
};
00C0C6D622471BA3003D8AF0 /* Platform */ = {
@ -166,9 +181,9 @@
children = (
00C0C6D022470705003D8AF0 /* IosCanvas.swift */,
00C0C6A7224654A2003D8AF0 /* IosDatabase.swift */,
00C0C6DA2247E6B0003D8AF0 /* IosDates.swift */,
00C0C6CD2246EFB3003D8AF0 /* IosExtensions.swift */,
00C0C6A9224654F4003D8AF0 /* IosFiles.swift */,
00C0C6DA2247E6B0003D8AF0 /* IosDates.swift */,
);
path = Platform;
sourceTree = "<group>";
@ -176,10 +191,10 @@
00C0C6D722472BC9003D8AF0 /* Platform */ = {
isa = PBXGroup;
children = (
00C0C6A122465365003D8AF0 /* IosFilesTest.swift */,
00C0C6A222465365003D8AF0 /* IosSqlDatabaseTest.swift */,
00C0C6D82247DC13003D8AF0 /* IosCanvasTest.swift */,
00C0C6A222465365003D8AF0 /* IosDatabaseTest.swift */,
00C0C6DC2247E6C4003D8AF0 /* IosDatesTest.swift */,
00C0C6A122465365003D8AF0 /* IosFilesTest.swift */,
);
path = Platform;
sourceTree = "<group>";
@ -270,6 +285,7 @@
00C0C6BD22465F65003D8AF0 /* fonts in Resources */,
00C0C6BE22465F65003D8AF0 /* databases in Resources */,
00C0C6BF22465F65003D8AF0 /* migrations in Resources */,
0057EC2B224C4CDB00C49288 /* icons in Resources */,
00A5B42F22009F5A0024E00C /* Assets.xcassets in Resources */,
00D48BD12200A31300CC4527 /* Launch.storyboard in Resources */,
);
@ -313,6 +329,7 @@
00C0C6AA224654F4003D8AF0 /* IosFiles.swift in Sources */,
00C0C6D122470705003D8AF0 /* IosCanvas.swift in Sources */,
00C0C6CE2246EFB3003D8AF0 /* IosExtensions.swift in Sources */,
00C0C6E0224A3602003D8AF0 /* ShowHabitController.swift in Sources */,
00C0C6A8224654A2003D8AF0 /* IosDatabase.swift in Sources */,
00C0C6DB2247E6B0003D8AF0 /* IosDates.swift in Sources */,
00A5B42A22009F590024E00C /* ListHabitsController.swift in Sources */,
@ -328,7 +345,7 @@
00C0C6DD2247E6C4003D8AF0 /* IosDatesTest.swift in Sources */,
00C0C6A52246537A003D8AF0 /* IosFilesTest.swift in Sources */,
00C0C6D92247DC13003D8AF0 /* IosCanvasTest.swift in Sources */,
00C0C6A62246537E003D8AF0 /* IosSqlDatabaseTest.swift in Sources */,
00C0C6A62246537E003D8AF0 /* IosDatabaseTest.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

Loading…
Cancel
Save