diff --git a/core/assets/main/databases/template.db b/core/assets/main/databases/template.db index 05c67f527..37e613a7b 100644 Binary files a/core/assets/main/databases/template.db and b/core/assets/main/databases/template.db differ diff --git a/core/assets/main/fonts/NotoSans-Bold.ttf b/core/assets/main/fonts/NotoSans-Bold.ttf new file mode 100755 index 000000000..987da8c9b Binary files /dev/null and b/core/assets/main/fonts/NotoSans-Bold.ttf differ diff --git a/core/assets/main/fonts/NotoSans-Regular.ttf b/core/assets/main/fonts/NotoSans-Regular.ttf new file mode 100755 index 000000000..79a438a53 Binary files /dev/null and b/core/assets/main/fonts/NotoSans-Regular.ttf differ diff --git a/core/assets/main/fonts/Roboto-Bold.ttf b/core/assets/main/fonts/Roboto-Bold.ttf deleted file mode 100755 index d3f01ad24..000000000 Binary files a/core/assets/main/fonts/Roboto-Bold.ttf and /dev/null differ diff --git a/core/assets/main/fonts/Roboto-Regular.ttf b/core/assets/main/fonts/Roboto-Regular.ttf deleted file mode 100755 index 2c97eeadf..000000000 Binary files a/core/assets/main/fonts/Roboto-Regular.ttf and /dev/null differ diff --git a/core/assets/test/components/CalendarChart/base.png b/core/assets/test/components/CalendarChart/base.png new file mode 100644 index 000000000..a42177547 Binary files /dev/null and b/core/assets/test/components/CalendarChart/base.png differ diff --git a/core/assets/test/components/CalendarChart/scroll.png b/core/assets/test/components/CalendarChart/scroll.png new file mode 100644 index 000000000..691599a90 Binary files /dev/null and b/core/assets/test/components/CalendarChart/scroll.png differ diff --git a/core/assets/test/components/old/habits/show/FrequencyCard/render.png b/core/assets/test/components/old/habits/show/FrequencyCard/render.png deleted file mode 100644 index 81399e360..000000000 Binary files a/core/assets/test/components/old/habits/show/FrequencyCard/render.png and /dev/null differ diff --git a/core/assets/test/components/old/habits/show/HistoryCard/render.png b/core/assets/test/components/old/habits/show/HistoryCard/render.png deleted file mode 100644 index 8c0cbc9d1..000000000 Binary files a/core/assets/test/components/old/habits/show/HistoryCard/render.png and /dev/null differ diff --git a/core/assets/test/components/old/habits/show/OverviewCard/render.png b/core/assets/test/components/old/habits/show/OverviewCard/render.png deleted file mode 100644 index 1f1ccc9f7..000000000 Binary files a/core/assets/test/components/old/habits/show/OverviewCard/render.png and /dev/null differ diff --git a/core/assets/test/components/old/habits/show/ScoreCard/render.png b/core/assets/test/components/old/habits/show/ScoreCard/render.png deleted file mode 100644 index 12aaa2785..000000000 Binary files a/core/assets/test/components/old/habits/show/ScoreCard/render.png and /dev/null differ diff --git a/core/assets/test/components/old/habits/show/StreakCard/render.png b/core/assets/test/components/old/habits/show/StreakCard/render.png deleted file mode 100644 index 07d5191fa..000000000 Binary files a/core/assets/test/components/old/habits/show/StreakCard/render.png and /dev/null differ diff --git a/core/assets/test/components/old/habits/show/SubtitleCard/render.png b/core/assets/test/components/old/habits/show/SubtitleCard/render.png deleted file mode 100644 index 17d4c44ab..000000000 Binary files a/core/assets/test/components/old/habits/show/SubtitleCard/render.png and /dev/null differ diff --git a/core/build.gradle b/core/build.gradle index b4157290f..8f9db33da 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -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" } } diff --git a/core/src/commonMain/kotlin/org/isoron/platform/concurrency/Tasks.kt b/core/src/commonMain/kotlin/org/isoron/platform/concurrency/Tasks.kt new file mode 100644 index 000000000..8f518d2b2 --- /dev/null +++ b/core/src/commonMain/kotlin/org/isoron/platform/concurrency/Tasks.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * 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 . + */ + +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 + + 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() + + 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) +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/gui/Canvas.kt b/core/src/commonMain/kotlin/org/isoron/platform/gui/Canvas.kt similarity index 90% rename from core/src/commonMain/kotlin/org/isoron/uhabits/gui/Canvas.kt rename to core/src/commonMain/kotlin/org/isoron/platform/gui/Canvas.kt index 27b7f50cb..77ecaba7a 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/gui/Canvas.kt +++ b/core/src/commonMain/kotlin/org/isoron/platform/gui/Canvas.kt @@ -17,7 +17,12 @@ * with this program. If not, see . */ -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) } \ No newline at end of file diff --git a/core/src/commonMain/kotlin/org/isoron/platform/gui/Colors.kt b/core/src/commonMain/kotlin/org/isoron/platform/gui/Colors.kt new file mode 100644 index 000000000..4161c1218 --- /dev/null +++ b/core/src/commonMain/kotlin/org/isoron/platform/gui/Colors.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * 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 . + */ + +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) + } +} diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/gui/components/Component.kt b/core/src/commonMain/kotlin/org/isoron/platform/gui/Component.kt similarity index 91% rename from core/src/commonMain/kotlin/org/isoron/uhabits/gui/components/Component.kt rename to core/src/commonMain/kotlin/org/isoron/platform/gui/Component.kt index b9ce759e8..d7ab56a68 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/gui/components/Component.kt +++ b/core/src/commonMain/kotlin/org/isoron/platform/gui/Component.kt @@ -17,9 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.gui.components - -import org.isoron.uhabits.gui.* +package org.isoron.platform.gui interface Component { fun draw(canvas: Canvas) diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/gui/FontAwesome.kt b/core/src/commonMain/kotlin/org/isoron/platform/gui/FontAwesome.kt similarity index 96% rename from core/src/commonMain/kotlin/org/isoron/uhabits/gui/FontAwesome.kt rename to core/src/commonMain/kotlin/org/isoron/platform/gui/FontAwesome.kt index 01e1d151f..3791228df 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/gui/FontAwesome.kt +++ b/core/src/commonMain/kotlin/org/isoron/platform/gui/FontAwesome.kt @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.gui +package org.isoron.platform.gui class FontAwesome { companion object { diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Database.kt b/core/src/commonMain/kotlin/org/isoron/platform/io/Database.kt similarity index 88% rename from core/src/commonMain/kotlin/org/isoron/uhabits/utils/Database.kt rename to core/src/commonMain/kotlin/org/isoron/platform/io/Database.kt index 98dfdbb5c..9d4b957e9 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Database.kt +++ b/core/src/commonMain/kotlin/org/isoron/platform/io/Database.kt @@ -17,35 +17,37 @@ * with this program. If not, see . */ -package org.isoron.uhabits.utils - -enum class StepResult { - ROW, - DONE -} +package org.isoron.platform.io interface PreparedStatement { fun step(): StepResult fun finalize() fun getInt(index: Int): Int + fun getLong(index: Int): Long fun getText(index: Int): String fun getReal(index: Int): Double fun bindInt(index: Int, value: Int) + fun bindLong(index: Int, value: Long) fun bindText(index: Int, value: String) fun bindReal(index: Int, value: Double) fun reset() } -interface Database { - fun prepareStatement(sql: String): PreparedStatement - fun close() +enum class StepResult { + ROW, + DONE } interface DatabaseOpener { fun open(file: UserFile): Database } -fun Database.execute(sql: String) { +interface Database { + fun prepareStatement(sql: String): PreparedStatement + fun close() +} + +fun Database.runInBackground(sql: String) { val stmt = prepareStatement(sql) stmt.step() stmt.finalize() @@ -70,13 +72,13 @@ fun Database.nextId(tableName: String): Int { } } -fun Database.begin() = execute("begin") +fun Database.begin() = runInBackground("begin") -fun Database.commit() = execute("commit") +fun Database.commit() = runInBackground("commit") fun Database.getVersion() = queryInt("pragma user_version") -fun Database.setVersion(v: Int) = execute("pragma user_version = $v") +fun Database.setVersion(v: Int) = runInBackground("pragma user_version = $v") fun Database.migrateTo(newVersion: Int, fileOpener: FileOpener, log: Log) { val currentVersion = getVersion() @@ -90,12 +92,11 @@ fun Database.migrateTo(newVersion: Int, fileOpener: FileOpener, log: Log) { begin() for (v in (currentVersion + 1)..newVersion) { - log.debug("Database", "Running migration $v") val filename = sprintf("migrations/%03d.sql", v) val migrationFile = fileOpener.openResourceFile(filename) for (line in migrationFile.readLines()) { if (line.isEmpty()) continue - execute(line) + runInBackground(line) } setVersion(v) } diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Files.kt b/core/src/commonMain/kotlin/org/isoron/platform/io/Files.kt similarity index 96% rename from core/src/commonMain/kotlin/org/isoron/uhabits/utils/Files.kt rename to core/src/commonMain/kotlin/org/isoron/platform/io/Files.kt index ee72de9e8..5be53ee5e 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Files.kt +++ b/core/src/commonMain/kotlin/org/isoron/platform/io/Files.kt @@ -17,25 +17,7 @@ * with this program. If not, see . */ -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 -} - -/** - * 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 + fun copyTo(dest: UserFile) +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Log.kt b/core/src/commonMain/kotlin/org/isoron/platform/io/Log.kt similarity index 97% rename from core/src/commonMain/kotlin/org/isoron/uhabits/utils/Log.kt rename to core/src/commonMain/kotlin/org/isoron/platform/io/Log.kt index be5f0d3fc..cc59eb6c9 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Log.kt +++ b/core/src/commonMain/kotlin/org/isoron/platform/io/Log.kt @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.utils +package org.isoron.platform.io interface Log { fun info(tag: String, msg: String) diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Strings.kt b/core/src/commonMain/kotlin/org/isoron/platform/io/Strings.kt similarity index 89% rename from core/src/commonMain/kotlin/org/isoron/uhabits/utils/Strings.kt rename to core/src/commonMain/kotlin/org/isoron/platform/io/Strings.kt index 9139da30c..986dea9df 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Strings.kt +++ b/core/src/commonMain/kotlin/org/isoron/platform/io/Strings.kt @@ -17,6 +17,6 @@ * with this program. If not, see . */ -package org.isoron.uhabits.utils +package org.isoron.platform.io expect fun sprintf(format: String, vararg args: Any?): String \ No newline at end of file diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Dates.kt b/core/src/commonMain/kotlin/org/isoron/platform/time/Dates.kt similarity index 51% rename from core/src/commonMain/kotlin/org/isoron/uhabits/utils/Dates.kt rename to core/src/commonMain/kotlin/org/isoron/platform/time/Dates.kt index 93d05291c..d05fd2fb0 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Dates.kt +++ b/core/src/commonMain/kotlin/org/isoron/platform/time/Dates.kt @@ -17,13 +17,37 @@ * with this program. If not, see . */ -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 } \ No newline at end of file diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/Backend.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/Backend.kt deleted file mode 100644 index 8e7a0f1d6..000000000 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/Backend.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2016-2019 Álinson Santos Xavier - * - * 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 . - */ - -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() - 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> { - 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) - } -} diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/backend/Backend.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/backend/Backend.kt new file mode 100644 index 000000000..12310d3b9 --- /dev/null +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/backend/Backend.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * 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 . + */ + +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() + + val checkmarks = mutableMapOf() + + 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) + } + } + + +} diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/backend/MainScreenDataSource.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/backend/MainScreenDataSource.kt new file mode 100644 index 000000000..4f03d11a5 --- /dev/null +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/backend/MainScreenDataSource.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * 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 . + */ + +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, + val checkmarks: MutableMap, + val taskRunner: TaskRunner) { + + private val today = LocalDate(2019, 3, 30) + + data class Data(val ids: List, + val scores: List, + val names: List, + val colors: List, + val checkmarks: List>) + + private val listeners = mutableListOf() + + 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) + } + } + } + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/components/CalendarChart.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/components/CalendarChart.kt new file mode 100644 index 000000000..09c149830 --- /dev/null +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/components/CalendarChart.kt @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * 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 . + */ + +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() + 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) + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/gui/components/CheckmarkButton.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/components/CheckmarkButton.kt similarity index 88% rename from core/src/commonMain/kotlin/org/isoron/uhabits/gui/components/CheckmarkButton.kt rename to core/src/commonMain/kotlin/org/isoron/uhabits/components/CheckmarkButton.kt index 32a88ed0a..5fdf99de0 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/gui/components/CheckmarkButton.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/components/CheckmarkButton.kt @@ -17,17 +17,16 @@ * with this program. If not, see . */ -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 diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/gui/components/HabitListHeader.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/components/HabitListHeader.kt similarity index 90% rename from core/src/commonMain/kotlin/org/isoron/uhabits/gui/components/HabitListHeader.kt rename to core/src/commonMain/kotlin/org/isoron/uhabits/components/HabitListHeader.kt index 3879b04f3..6d4ec6f28 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/gui/components/HabitListHeader.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/components/HabitListHeader.kt @@ -17,11 +17,10 @@ * with this program. If not, see . */ -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) diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/gui/components/NumberButton.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/components/NumberButton.kt similarity index 90% rename from core/src/commonMain/kotlin/org/isoron/uhabits/gui/components/NumberButton.kt rename to core/src/commonMain/kotlin/org/isoron/uhabits/components/NumberButton.kt index d692dbecf..f228ae1fa 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/gui/components/NumberButton.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/components/NumberButton.kt @@ -17,11 +17,10 @@ * with this program. If not, see . */ -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) } diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/gui/components/Ring.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/components/Ring.kt similarity index 87% rename from core/src/commonMain/kotlin/org/isoron/uhabits/gui/components/Ring.kt rename to core/src/commonMain/kotlin/org/isoron/uhabits/components/Ring.kt index 24d18c7d2..f18088022 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/gui/components/Ring.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/components/Ring.kt @@ -17,10 +17,10 @@ * with this program. If not, see . */ -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) } } } \ No newline at end of file diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/gui/Themes.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/components/Themes.kt similarity index 95% rename from core/src/commonMain/kotlin/org/isoron/uhabits/gui/Themes.kt rename to core/src/commonMain/kotlin/org/isoron/uhabits/components/Themes.kt index 840936377..a2d5eedc0 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/gui/Themes.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/components/Themes.kt @@ -17,7 +17,9 @@ * with this program. If not, see . */ -package org.isoron.uhabits.gui +package org.isoron.uhabits.components + +import org.isoron.platform.gui.* abstract class Theme { val toolbarColor = Color(0xffffff) diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/gui/Colors.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/gui/Colors.kt deleted file mode 100644 index 5940695d4..000000000 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/gui/Colors.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2016-2019 Álinson Santos Xavier - * - * 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 . - */ - -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 -} diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/models/Checkmark.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/models/Checkmark.kt index 701effa06..f2fd97673 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/models/Checkmark.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/models/Checkmark.kt @@ -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 + } +} diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/models/CheckmarkList.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/models/CheckmarkList.kt new file mode 100644 index 000000000..6856a3ae3 --- /dev/null +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/models/CheckmarkList.kt @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * 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 . + */ + +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() + private val automaticCheckmarks = mutableListOf() + + /** + * Replaces the entire list of manual checkmarks by the ones provided. The + * list of automatic checkmarks will be automatically updated. + */ + fun setManualCheckmarks(checks: List) { + 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 { + if (automaticCheckmarks.isEmpty()) return listOf() + + val result = mutableListOf() + 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, + frequency: Frequency, + calc: LocalDateCalculator + ): MutableList { + + 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, + 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, + intervals: List, + calc: LocalDateCalculator + ): MutableList { + + 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() + 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, + frequency: Frequency, + calc: LocalDateCalculator): MutableList { + + val num = frequency.numerator + val den = frequency.denominator + + val intervals = mutableListOf() + 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) +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/models/CheckmarkRepository.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/models/CheckmarkRepository.kt new file mode 100644 index 000000000..169ea4f0c --- /dev/null +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/models/CheckmarkRepository.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * 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 . + */ + +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 { + findStatement.bindInt(0, habitId) + val result = mutableListOf() + 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() + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/models/Frequency.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/models/Frequency.kt index 142294601..e07ef0e38 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/models/Frequency.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/models/Frequency.kt @@ -20,4 +20,10 @@ package org.isoron.uhabits.models data class Frequency(val numerator: Int, - val denominator: Int) \ No newline at end of file + val denominator: Int) { + companion object { + val WEEKLY = Frequency(1, 7) + val DAILY = Frequency(1, 1) + val TWO_TIMES_PER_WEEK = Frequency(2, 7) + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/models/Habit.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/models/Habit.kt index cb72b2af7..7ddfa6188 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/models/Habit.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/models/Habit.kt @@ -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, diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/models/HabitRepository.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/models/HabitRepository.kt index 3581e580b..f66520cfb 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/models/HabitRepository.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/models/HabitRepository.kt @@ -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) { diff --git a/core/src/commonTest/kotlin/org/isoron/uhabits/utils/StringsTest.kt b/core/src/commonTest/kotlin/org/isoron/platform/io/StringsTest.kt similarity index 93% rename from core/src/commonTest/kotlin/org/isoron/uhabits/utils/StringsTest.kt rename to core/src/commonTest/kotlin/org/isoron/platform/io/StringsTest.kt index 5b0cb3ce4..1191452e7 100644 --- a/core/src/commonTest/kotlin/org/isoron/uhabits/utils/StringsTest.kt +++ b/core/src/commonTest/kotlin/org/isoron/platform/io/StringsTest.kt @@ -17,8 +17,9 @@ * with this program. If not, see . */ -package org.isoron.uhabits.utils +package org.isoron.platform.io +import org.isoron.platform.io.* import kotlin.test.Test import kotlin.test.assertEquals diff --git a/core/src/iosMain/kotlin/org/isoron/uhabits/utils/Strings.kt b/core/src/iosMain/kotlin/org/isoron/platform/io/Strings.kt similarity index 96% rename from core/src/iosMain/kotlin/org/isoron/uhabits/utils/Strings.kt rename to core/src/iosMain/kotlin/org/isoron/platform/io/Strings.kt index 00126c451..bda11de60 100644 --- a/core/src/iosMain/kotlin/org/isoron/uhabits/utils/Strings.kt +++ b/core/src/iosMain/kotlin/org/isoron/platform/io/Strings.kt @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.utils +package org.isoron.platform.io import kotlinx.cinterop.* diff --git a/core/src/jvmMain/kotlin/org/isoron/uhabits/gui/JavaCanvas.kt b/core/src/jvmMain/kotlin/org/isoron/platform/gui/JavaCanvas.kt similarity index 75% rename from core/src/jvmMain/kotlin/org/isoron/uhabits/gui/JavaCanvas.kt rename to core/src/jvmMain/kotlin/org/isoron/platform/gui/JavaCanvas.kt index 35081db90..c3aba7003 100644 --- a/core/src/jvmMain/kotlin/org/isoron/uhabits/gui/JavaCanvas.kt +++ b/core/src/jvmMain/kotlin/org/isoron/platform/gui/JavaCanvas.kt @@ -17,31 +17,32 @@ * with this program. If not, see . */ -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 + } } \ No newline at end of file diff --git a/core/src/jvmMain/kotlin/org/isoron/uhabits/utils/JavaDatabase.kt b/core/src/jvmMain/kotlin/org/isoron/platform/io/JavaDatabase.kt similarity index 86% rename from core/src/jvmMain/kotlin/org/isoron/uhabits/utils/JavaDatabase.kt rename to core/src/jvmMain/kotlin/org/isoron/platform/io/JavaDatabase.kt index a0a52ce5f..8a562b0ea 100644 --- a/core/src/jvmMain/kotlin/org/isoron/uhabits/utils/JavaDatabase.kt +++ b/core/src/jvmMain/kotlin/org/isoron/platform/io/JavaDatabase.kt @@ -17,14 +17,12 @@ * with this program. If not, see . */ -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() } diff --git a/core/src/jvmMain/kotlin/org/isoron/uhabits/utils/JavaFiles.kt b/core/src/jvmMain/kotlin/org/isoron/platform/io/JavaFiles.kt similarity index 92% rename from core/src/jvmMain/kotlin/org/isoron/uhabits/utils/JavaFiles.kt rename to core/src/jvmMain/kotlin/org/isoron/platform/io/JavaFiles.kt index ebb23a342..b87565a03 100644 --- a/core/src/jvmMain/kotlin/org/isoron/uhabits/utils/JavaFiles.kt +++ b/core/src/jvmMain/kotlin/org/isoron/platform/io/JavaFiles.kt @@ -17,14 +17,16 @@ * with this program. If not, see . */ -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 { return Files.readAllLines(path) } diff --git a/core/src/jvmMain/kotlin/org/isoron/uhabits/utils/Strings.kt b/core/src/jvmMain/kotlin/org/isoron/platform/io/Strings.kt similarity index 96% rename from core/src/jvmMain/kotlin/org/isoron/uhabits/utils/Strings.kt rename to core/src/jvmMain/kotlin/org/isoron/platform/io/Strings.kt index 8bc26773c..de3b612c7 100644 --- a/core/src/jvmMain/kotlin/org/isoron/uhabits/utils/Strings.kt +++ b/core/src/jvmMain/kotlin/org/isoron/platform/io/Strings.kt @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.utils +package org.isoron.platform.io actual fun sprintf(format: String, vararg args: Any?): String { return String.format(format, *args) diff --git a/core/src/jvmMain/kotlin/org/isoron/platform/time/JavaDates.kt b/core/src/jvmMain/kotlin/org/isoron/platform/time/JavaDates.kt new file mode 100644 index 000000000..ae8c8e958 --- /dev/null +++ b/core/src/jvmMain/kotlin/org/isoron/platform/time/JavaDates.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * 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 . + */ + +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() + } +} \ No newline at end of file diff --git a/core/src/jvmMain/kotlin/org/isoron/uhabits/utils/JavaDates.kt b/core/src/jvmMain/kotlin/org/isoron/uhabits/utils/JavaDates.kt deleted file mode 100644 index 4fa4cdfda..000000000 --- a/core/src/jvmMain/kotlin/org/isoron/uhabits/utils/JavaDates.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) 2016-2019 Álinson Santos Xavier - * - * 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 . - */ - -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)) - } -} \ No newline at end of file diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/gui/JavaCanvasTest.kt b/core/src/jvmTest/kotlin/org/isoron/platform/JavaCanvasTest.kt similarity index 85% rename from core/src/jvmTest/kotlin/org/isoron/uhabits/gui/JavaCanvasTest.kt rename to core/src/jvmTest/kotlin/org/isoron/platform/JavaCanvasTest.kt index 69939f47a..673a406b9 100644 --- a/core/src/jvmTest/kotlin/org/isoron/uhabits/gui/JavaCanvasTest.kt +++ b/core/src/jvmTest/kotlin/org/isoron/platform/JavaCanvasTest.kt @@ -17,8 +17,9 @@ * with this program. If not, see . */ -package org.isoron.uhabits.gui +package org.isoron.platform +import org.isoron.platform.gui.* import org.junit.* import java.awt.image.* import java.io.* @@ -54,12 +55,17 @@ class JavaCanvasTest { canvas.drawLine(0.0, 0.0, 500.0, 400.0) canvas.drawLine(500.0, 0.0, 0.0, 400.0) - canvas.setTextSize(50.0) + canvas.setFont(Font.BOLD) + canvas.setFontSize(50.0) canvas.setColor(Color(0x00FF00)) - canvas.drawText("Test", 250.0, 200.0) + canvas.setTextAlign(TextAlign.CENTER) + canvas.drawText("HELLO", 250.0, 100.0) - canvas.setFont(Font.BOLD) - canvas.drawText("Test", 250.0, 100.0) + canvas.setTextAlign(TextAlign.RIGHT) + canvas.drawText("HELLO", 250.0, 150.0) + + canvas.setTextAlign(TextAlign.LEFT) + canvas.drawText("HELLO", 250.0, 200.0) canvas.setFont(Font.FONT_AWESOME) canvas.drawText(FontAwesome.CHECK, 250.0, 300.0) diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/database/JavaDatabaseTest.kt b/core/src/jvmTest/kotlin/org/isoron/platform/JavaDatabaseTest.kt similarity index 95% rename from core/src/jvmTest/kotlin/org/isoron/uhabits/database/JavaDatabaseTest.kt rename to core/src/jvmTest/kotlin/org/isoron/platform/JavaDatabaseTest.kt index 3400b95e1..261698d7e 100644 --- a/core/src/jvmTest/kotlin/org/isoron/uhabits/database/JavaDatabaseTest.kt +++ b/core/src/jvmTest/kotlin/org/isoron/platform/JavaDatabaseTest.kt @@ -17,11 +17,10 @@ * with this program. If not, see . */ -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 diff --git a/core/src/jvmTest/kotlin/org/isoron/platform/JavaDatesTest.kt b/core/src/jvmTest/kotlin/org/isoron/platform/JavaDatesTest.kt new file mode 100644 index 000000000..d88dfe577 --- /dev/null +++ b/core/src/jvmTest/kotlin/org/isoron/platform/JavaDatesTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * 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 . + */ + +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)) + } +} diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/utils/JavaFilesTest.kt b/core/src/jvmTest/kotlin/org/isoron/platform/JavaFilesTest.kt similarity index 97% rename from core/src/jvmTest/kotlin/org/isoron/uhabits/utils/JavaFilesTest.kt rename to core/src/jvmTest/kotlin/org/isoron/platform/JavaFilesTest.kt index e0b712a90..b98facb47 100644 --- a/core/src/jvmTest/kotlin/org/isoron/uhabits/utils/JavaFilesTest.kt +++ b/core/src/jvmTest/kotlin/org/isoron/platform/JavaFilesTest.kt @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.utils +package org.isoron.platform import org.isoron.uhabits.BaseTest import org.junit.Test diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/BackendTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/BackendTest.kt deleted file mode 100644 index d4eb10f19..000000000 --- a/core/src/jvmTest/kotlin/org/isoron/uhabits/BackendTest.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2016-2019 Álinson Santos Xavier - * - * 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 . - */ - -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") - } -} \ No newline at end of file diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/Base.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/Base.kt index 15b10a16e..e4a99dd18 100644 --- a/core/src/jvmTest/kotlin/org/isoron/uhabits/Base.kt +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/Base.kt @@ -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}.") } } } \ No newline at end of file diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/backend/BackendTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/backend/BackendTest.kt new file mode 100644 index 000000000..5c845e194 --- /dev/null +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/backend/BackendTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * 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 . + */ + +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)) + } +} diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/components/CalendarChartTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/components/CalendarChartTest.kt new file mode 100644 index 000000000..3474d0658 --- /dev/null +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/components/CalendarChartTest.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * 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 . + */ + +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) + } +} \ No newline at end of file diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/gui/components/CheckmarkButtonTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/components/CheckmarkButtonTest.kt similarity index 93% rename from core/src/jvmTest/kotlin/org/isoron/uhabits/gui/components/CheckmarkButtonTest.kt rename to core/src/jvmTest/kotlin/org/isoron/uhabits/components/CheckmarkButtonTest.kt index a7ca9377d..91714e88e 100644 --- a/core/src/jvmTest/kotlin/org/isoron/uhabits/gui/components/CheckmarkButtonTest.kt +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/components/CheckmarkButtonTest.kt @@ -17,10 +17,9 @@ * with this program. If not, see . */ -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() { diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/gui/components/HabitListHeaderTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/components/HabitListHeaderTest.kt similarity index 88% rename from core/src/jvmTest/kotlin/org/isoron/uhabits/gui/components/HabitListHeaderTest.kt rename to core/src/jvmTest/kotlin/org/isoron/uhabits/components/HabitListHeaderTest.kt index cd72d4c07..6086575cc 100644 --- a/core/src/jvmTest/kotlin/org/isoron/uhabits/gui/components/HabitListHeaderTest.kt +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/components/HabitListHeaderTest.kt @@ -17,12 +17,10 @@ * with this program. If not, see . */ -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.* diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/gui/components/NumberButtonTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/components/NumberButtonTest.kt similarity index 96% rename from core/src/jvmTest/kotlin/org/isoron/uhabits/gui/components/NumberButtonTest.kt rename to core/src/jvmTest/kotlin/org/isoron/uhabits/components/NumberButtonTest.kt index 2db7afcb1..47228f016 100644 --- a/core/src/jvmTest/kotlin/org/isoron/uhabits/gui/components/NumberButtonTest.kt +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/components/NumberButtonTest.kt @@ -17,11 +17,10 @@ * with this program. If not, see . */ -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.* diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/gui/components/RingTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/components/RingTest.kt similarity index 92% rename from core/src/jvmTest/kotlin/org/isoron/uhabits/gui/components/RingTest.kt rename to core/src/jvmTest/kotlin/org/isoron/uhabits/components/RingTest.kt index 4141762bd..e017d8737 100644 --- a/core/src/jvmTest/kotlin/org/isoron/uhabits/gui/components/RingTest.kt +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/components/RingTest.kt @@ -17,10 +17,9 @@ * with this program. If not, see . */ -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() { diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/models/CheckmarkListTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/models/CheckmarkListTest.kt new file mode 100644 index 000000000..b85650784 --- /dev/null +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/models/CheckmarkListTest.kt @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * 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 . + */ + +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() + assertEquals(expected, list.getValuesUntil(day(0))) + } +} \ No newline at end of file diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/models/CheckmarkRepositoryTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/models/CheckmarkRepositoryTest.kt new file mode 100644 index 000000000..8f8414621 --- /dev/null +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/models/CheckmarkRepositoryTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * 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 . + */ + +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(), 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)) + } +} \ No newline at end of file diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/models/HabitRepositoryTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/models/HabitRepositoryTest.kt index 631702e65..013eb40a4 100644 --- a/core/src/jvmTest/kotlin/org/isoron/uhabits/models/HabitRepositoryTest.kt +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/models/HabitRepositoryTest.kt @@ -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) diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/utils/JavaDatesTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/utils/JavaDatesTest.kt deleted file mode 100644 index aec6bd1fc..000000000 --- a/core/src/jvmTest/kotlin/org/isoron/uhabits/utils/JavaDatesTest.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2016-2019 Álinson Santos Xavier - * - * 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 . - */ - -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)), "日") - } -} \ No newline at end of file diff --git a/ios/Application/AppDelegate.swift b/ios/Application/AppDelegate.swift index 9309caebe..5656064d4 100644 --- a/ios/Application/AppDelegate.swift +++ b/ios/Application/AppDelegate.swift @@ -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 { diff --git a/ios/Application/EditHabitController.swift b/ios/Application/Frontend/EditHabitController.swift similarity index 100% rename from ios/Application/EditHabitController.swift rename to ios/Application/Frontend/EditHabitController.swift diff --git a/ios/Application/ListHabitsController.swift b/ios/Application/Frontend/ListHabitsController.swift similarity index 78% rename from ios/Application/ListHabitsController.swift rename to ios/Application/Frontend/ListHabitsController.swift index 9b1653b94..103885541 100644 --- a/ios/Application/ListHabitsController.swift +++ b/ios/Application/Frontend/ListHabitsController.swift @@ -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) + } } diff --git a/ios/Application/Frontend/ShowHabitController.swift b/ios/Application/Frontend/ShowHabitController.swift new file mode 100644 index 000000000..352f7170a --- /dev/null +++ b/ios/Application/Frontend/ShowHabitController.swift @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * 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 . + */ + +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 + } +} diff --git a/ios/Application/Platform/IosCanvas.swift b/ios/Application/Platform/IosCanvas.swift index f9697fabf..e68241851 100644 --- a/ios/Application/Platform/IosCanvas.swift +++ b/ios/Application/Platform/IosCanvas.swift @@ -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 + } } diff --git a/ios/Application/Platform/IosDatabase.swift b/ios/Application/Platform/IosDatabase.swift index bb629b44b..62e9833da 100644 --- a/ios/Application/Platform/IosDatabase.swift +++ b/ios/Application/Platform/IosDatabase.swift @@ -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 { diff --git a/ios/Application/Platform/IosDates.swift b/ios/Application/Platform/IosDates.swift index dacf2170a..9c2da3be5 100644 --- a/ios/Application/Platform/IosDates.swift +++ b/ios/Application/Platform/IosDates.swift @@ -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) - } } diff --git a/ios/Application/Platform/IosExtensions.swift b/ios/Application/Platform/IosExtensions.swift index 21868652b..7cafb31d1 100644 --- a/ios/Application/Platform/IosExtensions.swift +++ b/ios/Application/Platform/IosExtensions.swift @@ -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 { diff --git a/ios/Application/Platform/IosFiles.swift b/ios/Application/Platform/IosFiles.swift index a242250b7..223b47b38 100644 --- a/ios/Application/Platform/IosFiles.swift +++ b/ios/Application/Platform/IosFiles.swift @@ -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 { diff --git a/ios/Assets/databases/template.db b/ios/Assets/databases/template.db deleted file mode 100644 index 05c67f527..000000000 Binary files a/ios/Assets/databases/template.db and /dev/null differ diff --git a/ios/Assets/fonts/FontAwesome.ttf b/ios/Assets/fonts/FontAwesome.ttf deleted file mode 100644 index e89738de5..000000000 Binary files a/ios/Assets/fonts/FontAwesome.ttf and /dev/null differ diff --git a/ios/Assets/migrations/001.sql b/ios/Assets/migrations/001.sql deleted file mode 100644 index e69de29bb..000000000 diff --git a/ios/Assets/migrations/002.sql b/ios/Assets/migrations/002.sql deleted file mode 100644 index e69de29bb..000000000 diff --git a/ios/Assets/migrations/003.sql b/ios/Assets/migrations/003.sql deleted file mode 100644 index e69de29bb..000000000 diff --git a/ios/Assets/migrations/004.sql b/ios/Assets/migrations/004.sql deleted file mode 100644 index e69de29bb..000000000 diff --git a/ios/Assets/migrations/005.sql b/ios/Assets/migrations/005.sql deleted file mode 100644 index e69de29bb..000000000 diff --git a/ios/Assets/migrations/006.sql b/ios/Assets/migrations/006.sql deleted file mode 100644 index e69de29bb..000000000 diff --git a/ios/Assets/migrations/007.sql b/ios/Assets/migrations/007.sql deleted file mode 100644 index e69de29bb..000000000 diff --git a/ios/Assets/migrations/008.sql b/ios/Assets/migrations/008.sql deleted file mode 100644 index e69de29bb..000000000 diff --git a/ios/Assets/migrations/009.sql b/ios/Assets/migrations/009.sql deleted file mode 100644 index 5a4afd962..000000000 --- a/ios/Assets/migrations/009.sql +++ /dev/null @@ -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 ) diff --git a/ios/Assets/migrations/010.sql b/ios/Assets/migrations/010.sql deleted file mode 100644 index afae84e20..000000000 --- a/ios/Assets/migrations/010.sql +++ /dev/null @@ -1,3 +0,0 @@ -delete from Score -delete from Streak -delete from Checkmarks \ No newline at end of file diff --git a/ios/Assets/migrations/011.sql b/ios/Assets/migrations/011.sql deleted file mode 100644 index 6ea4e2390..000000000 --- a/ios/Assets/migrations/011.sql +++ /dev/null @@ -1 +0,0 @@ -alter table Habits add column reminder_days integer not null default 127 \ No newline at end of file diff --git a/ios/Assets/migrations/012.sql b/ios/Assets/migrations/012.sql deleted file mode 100644 index afae84e20..000000000 --- a/ios/Assets/migrations/012.sql +++ /dev/null @@ -1,3 +0,0 @@ -delete from Score -delete from Streak -delete from Checkmarks \ No newline at end of file diff --git a/ios/Assets/migrations/013.sql b/ios/Assets/migrations/013.sql deleted file mode 100644 index dbb9b4438..000000000 --- a/ios/Assets/migrations/013.sql +++ /dev/null @@ -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) \ No newline at end of file diff --git a/ios/Assets/migrations/014.sql b/ios/Assets/migrations/014.sql deleted file mode 100644 index 6eecfc046..000000000 --- a/ios/Assets/migrations/014.sql +++ /dev/null @@ -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 \ No newline at end of file diff --git a/ios/Assets/migrations/015.sql b/ios/Assets/migrations/015.sql deleted file mode 100644 index afae84e20..000000000 --- a/ios/Assets/migrations/015.sql +++ /dev/null @@ -1,3 +0,0 @@ -delete from Score -delete from Streak -delete from Checkmarks \ No newline at end of file diff --git a/ios/Assets/migrations/016.sql b/ios/Assets/migrations/016.sql deleted file mode 100644 index 059f2016b..000000000 --- a/ios/Assets/migrations/016.sql +++ /dev/null @@ -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 \ No newline at end of file diff --git a/ios/Assets/migrations/017.sql b/ios/Assets/migrations/017.sql deleted file mode 100644 index 15430d771..000000000 --- a/ios/Assets/migrations/017.sql +++ /dev/null @@ -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 \ No newline at end of file diff --git a/ios/Assets/migrations/018.sql b/ios/Assets/migrations/018.sql deleted file mode 100644 index 4cdc9cc8d..000000000 --- a/ios/Assets/migrations/018.sql +++ /dev/null @@ -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 "" \ No newline at end of file diff --git a/ios/Assets/migrations/019.sql b/ios/Assets/migrations/019.sql deleted file mode 100644 index 0569ea531..000000000 --- a/ios/Assets/migrations/019.sql +++ /dev/null @@ -1 +0,0 @@ -create table Events ( id integer primary key autoincrement, timestamp integer, message text, server_id integer ) \ No newline at end of file diff --git a/ios/Assets/migrations/020.sql b/ios/Assets/migrations/020.sql deleted file mode 100644 index 254bb7b80..000000000 --- a/ios/Assets/migrations/020.sql +++ /dev/null @@ -1,3 +0,0 @@ -drop table checkmarks -drop table streak -drop table score diff --git a/ios/Assets/migrations/021.sql b/ios/Assets/migrations/021.sql deleted file mode 100644 index 547b6759d..000000000 --- a/ios/Assets/migrations/021.sql +++ /dev/null @@ -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 \ No newline at end of file diff --git a/ios/Assets/migrations/022.sql b/ios/Assets/migrations/022.sql deleted file mode 100644 index b9ca2ba5a..000000000 --- a/ios/Assets/migrations/022.sql +++ /dev/null @@ -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 \ No newline at end of file diff --git a/ios/Tests/Platform/IosSqlDatabaseTest.swift b/ios/Tests/Platform/IosDatabaseTest.swift similarity index 100% rename from ios/Tests/Platform/IosSqlDatabaseTest.swift rename to ios/Tests/Platform/IosDatabaseTest.swift diff --git a/ios/uhabits.xcodeproj/project.pbxproj b/ios/uhabits.xcodeproj/project.pbxproj index 12dbdb7fe..d8e74fd21 100644 --- a/ios/uhabits.xcodeproj/project.pbxproj +++ b/ios/uhabits.xcodeproj/project.pbxproj @@ -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 = ""; }; 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 = ""; }; 00A5B42922009F590024E00C /* ListHabitsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListHabitsController.swift; sourceTree = ""; }; @@ -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 = ""; }; 00C0C6A122465365003D8AF0 /* IosFilesTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IosFilesTest.swift; sourceTree = ""; }; - 00C0C6A222465365003D8AF0 /* IosSqlDatabaseTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IosSqlDatabaseTest.swift; sourceTree = ""; }; + 00C0C6A222465365003D8AF0 /* IosDatabaseTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IosDatabaseTest.swift; sourceTree = ""; }; 00C0C6A7224654A2003D8AF0 /* IosDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IosDatabase.swift; sourceTree = ""; }; 00C0C6A9224654F4003D8AF0 /* IosFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosFiles.swift; sourceTree = ""; }; 00C0C6AE224655D8003D8AF0 /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = ""; }; @@ -75,6 +78,7 @@ 00C0C6D82247DC13003D8AF0 /* IosCanvasTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosCanvasTest.swift; sourceTree = ""; }; 00C0C6DA2247E6B0003D8AF0 /* IosDates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosDates.swift; sourceTree = ""; }; 00C0C6DC2247E6C4003D8AF0 /* IosDatesTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosDatesTest.swift; sourceTree = ""; }; + 00C0C6DE224A35FC003D8AF0 /* ShowHabitController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowHabitController.swift; sourceTree = ""; }; 00D48BD02200A31300CC4527 /* Launch.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Launch.storyboard; sourceTree = ""; }; 00D48BD22200AC1600CC4527 /* EditHabitController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditHabitController.swift; sourceTree = ""; }; /* 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 = ""; + }; 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 = ""; }; 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 = ""; @@ -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 = ""; @@ -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; }; diff --git a/ios/uhabits.xcodeproj/project.xcworkspace/xcuserdata/isoron.xcuserdatad/UserInterfaceState.xcuserstate b/ios/uhabits.xcodeproj/project.xcworkspace/xcuserdata/isoron.xcuserdatad/UserInterfaceState.xcuserstate index 8daa212d3..174c153cf 100644 Binary files a/ios/uhabits.xcodeproj/project.xcworkspace/xcuserdata/isoron.xcuserdatad/UserInterfaceState.xcuserstate and b/ios/uhabits.xcodeproj/project.xcworkspace/xcuserdata/isoron.xcuserdatad/UserInterfaceState.xcuserstate differ