Reorganize packages; implement checkmarks
BIN
core/assets/main/fonts/NotoSans-Bold.ttf
Executable file
BIN
core/assets/main/fonts/NotoSans-Regular.ttf
Executable file
BIN
core/assets/test/components/CalendarChart/base.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
core/assets/test/components/CalendarChart/scroll.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 15 KiB |
@@ -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)
|
||||
}
|
||||
45
core/src/commonMain/kotlin/org/isoron/platform/gui/Colors.kt
Normal file
@@ -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,23 +17,29 @@
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.utils
|
||||
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()
|
||||
}
|
||||
|
||||
enum class StepResult {
|
||||
ROW,
|
||||
DONE
|
||||
}
|
||||
|
||||
interface PreparedStatement {
|
||||
fun step(): StepResult
|
||||
fun finalize()
|
||||
fun getInt(index: Int): Int
|
||||
fun getText(index: Int): String
|
||||
fun getReal(index: Int): Double
|
||||
fun bindInt(index: Int, value: Int)
|
||||
fun bindText(index: Int, value: String)
|
||||
fun bindReal(index: Int, value: Double)
|
||||
fun reset()
|
||||
interface DatabaseOpener {
|
||||
fun open(file: UserFile): Database
|
||||
}
|
||||
|
||||
interface Database {
|
||||
@@ -41,11 +47,7 @@ interface Database {
|
||||
fun close()
|
||||
}
|
||||
|
||||
interface DatabaseOpener {
|
||||
fun open(file: UserFile): Database
|
||||
}
|
||||
|
||||
fun Database.execute(sql: String) {
|
||||
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 {
|
||||
return plusDays(date, -days)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
100
core/src/commonMain/kotlin/org/isoron/uhabits/backend/Backend.kt
Normal file
@@ -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()
|
||||
|
||||
g2d.drawString(text,
|
||||
toPixel(x) - bx - bWidth / 2,
|
||||
toPixel(y) - by - bHeight / 2)
|
||||
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.setColor(Color(0x00FF00))
|
||||
canvas.drawText("Test", 250.0, 200.0)
|
||||
|
||||
canvas.setFont(Font.BOLD)
|
||||
canvas.drawText("Test", 250.0, 100.0)
|
||||
canvas.setFontSize(50.0)
|
||||
canvas.setColor(Color(0x00FF00))
|
||||
canvas.setTextAlign(TextAlign.CENTER)
|
||||
canvas.drawText("HELLO", 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
|
||||
|
||||
105
core/src/jvmTest/kotlin/org/isoron/platform/JavaDatesTest.kt
Normal file
@@ -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,
|
||||
target: self,
|
||||
action: #selector(self.onCreateHabitClicked))
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
91
ios/Application/Frontend/ShowHabitController.swift
Normal file
@@ -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)
|
||||
nsText.draw(at: CGPoint(x: CGFloat(x) - size.width / 2,
|
||||
y : CGFloat(y) - size.height / 2),
|
||||
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
|
||||
|
||||
@@ -47,6 +49,10 @@ class IosPreparedStatement : NSObject, PreparedStatement {
|
||||
func getInt(index: Int32) -> Int32 {
|
||||
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)!
|
||||
let fmt = DateFormatter()
|
||||
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 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)
|
||||
|
||||
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 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))
|
||||
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 {
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import Foundation
|
||||
|
||||
class IosResourceFile : NSObject, ResourceFile {
|
||||
|
||||
|
||||
var path: String
|
||||
var fileManager = FileManager.default
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||