Reorganize top level directory

This commit is contained in:
2021-01-03 14:43:49 -06:00
parent 9fd36d8d53
commit bebb356425
841 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
package = sqlite3
headers = sqlite3.h
headerFilter = sqlite3*.h
compilerOpts = -std=c11
linkerOpts.ios = -lsqlite3
excludedFunctions = sqlite3_mutex_held \
sqlite3_mutex_notheld \
sqlite3_snapshot_cmp \
sqlite3_snapshot_free \
sqlite3_snapshot_get \
sqlite3_snapshot_open \
sqlite3_snapshot_recover \
sqlite3_set_last_insert_rowid \
sqlite3_stmt_scanstatus \
sqlite3_stmt_scanstatus_reset \
sqlite3_column_database_name \
sqlite3_column_database_name16 \
sqlite3_column_origin_name \
sqlite3_column_origin_name16 \
sqlite3_column_table_name \
sqlite3_column_table_name16 \
sqlite3_enable_load_extension \
sqlite3_load_extension \
sqlite3_unlock_notify
noStringConversion = sqlite3_prepare_v2 sqlite3_prepare_v3

View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.concurrency
class Observable<T> {
private val listeners = mutableListOf<T>()
fun addListener(listener: T) {
listeners.add(listener)
}
fun notifyListeners(action: (T) -> Unit) {
for (l in listeners) action.invoke(l)
}
fun removeListener(listener: T) {
listeners.remove(listener)
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.gui
enum class TextAlign {
LEFT, CENTER, RIGHT
}
enum class Font {
REGULAR,
BOLD,
FONT_AWESOME
}
interface Canvas {
fun setColor(color: Color)
fun drawLine(x1: Double, y1: Double, x2: Double, y2: Double)
fun drawText(text: String, x: Double, y: Double)
fun fillRect(x: Double, y: Double, width: Double, height: Double)
fun drawRect(x: Double, y: Double, width: Double, height: Double)
fun getHeight(): Double
fun getWidth(): Double
fun setFont(font: Font)
fun setFontSize(size: Double)
fun setStrokeWidth(size: Double)
fun fillArc(centerX: Double,
centerY: Double,
radius: Double,
startAngle: Double,
swipeAngle: Double)
fun fillCircle(centerX: Double, centerY: Double, radius: Double)
fun setTextAlign(align: TextAlign)
fun toImage(): Image
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.gui
data class PaletteColor(val index: Int)
data class Color(val red: Double,
val green: Double,
val blue: Double,
val alpha: Double) {
val luminosity: Double
get() {
return 0.21 * red + 0.72 * green + 0.07 * blue
}
constructor(rgb: Int) : this(((rgb shr 16) and 0xFF) / 255.0,
((rgb shr 8) and 0xFF) / 255.0,
((rgb shr 0) and 0xFF) / 255.0,
1.0)
fun blendWith(other: Color, weight: Double): Color {
return Color(red * (1 - weight) + other.red * weight,
green * (1 - weight) + other.green * weight,
blue * (1 - weight) + other.blue * weight,
alpha * (1 - weight) + other.alpha * weight)
}
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.gui
interface Component {
fun draw(canvas: Canvas)
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.gui
class FontAwesome {
companion object {
val CHECK = "\uf00c"
val TIMES = "\uf00d"
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.gui
import kotlin.math.*
interface Image {
val width: Int
val height: Int
fun getPixel(x: Int, y: Int): Color
fun setPixel(x: Int, y: Int, color: Color)
suspend fun export(path: String)
fun diff(other: Image) {
if (width != other.width) error("Width must match: $width !== ${other.width}")
if (height != other.height) error("Height must match: $height !== ${other.height}")
for (x in 0 until width) {
for (y in 0 until height) {
val p1 = getPixel(x, y)
var l = 1.0
for (dx in -2..2) {
if (x + dx < 0 || x + dx >= width) continue
for (dy in -2..2) {
if (y + dy < 0 || y + dy >= height) continue
val p2 = other.getPixel(x + dx, y + dy)
l = min(l, abs(p1.luminosity - p2.luminosity))
}
}
setPixel(x, y, Color(l, l, l, 1.0))
}
}
}
val averageLuminosity: Double
get() {
var luminosity = 0.0
for (x in 0 until width) {
for (y in 0 until height) {
luminosity += getPixel(x, y).luminosity
}
}
return luminosity / (width * height)
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.io
interface PreparedStatement {
fun step(): StepResult
fun finalize()
fun getInt(index: Int): Int
fun getLong(index: Int): Long
fun getText(index: Int): String
fun getReal(index: Int): Double
fun bindInt(index: Int, value: Int)
fun bindLong(index: Int, value: Long)
fun bindText(index: Int, value: String)
fun bindReal(index: Int, value: Double)
fun reset()
}
enum class StepResult {
ROW,
DONE
}
interface DatabaseOpener {
fun open(file: UserFile): Database
}
interface Database {
fun prepareStatement(sql: String): PreparedStatement
fun close()
}
fun Database.run(sql: String) {
val stmt = prepareStatement(sql)
stmt.step()
stmt.finalize()
}
fun Database.queryInt(sql: String): Int {
val stmt = prepareStatement(sql)
stmt.step()
val result = stmt.getInt(0)
stmt.finalize()
return result
}
fun Database.nextId(tableName: String): Int {
val stmt = prepareStatement("select seq from sqlite_sequence where name='$tableName'")
if (stmt.step() == StepResult.ROW) {
val result = stmt.getInt(0)
stmt.finalize()
return result + 1
} else {
return 0
}
}
fun Database.begin() = run("begin")
fun Database.commit() = run("commit")
fun Database.getVersion() = queryInt("pragma user_version")
fun Database.setVersion(v: Int) = run("pragma user_version = $v")
suspend fun Database.migrateTo(newVersion: Int,
fileOpener: FileOpener,
log: Log) {
val currentVersion = getVersion()
log.debug("Database", "Current database version: $currentVersion")
if (currentVersion == newVersion) return
log.debug("Database", "Upgrading to version: $newVersion")
if (currentVersion > newVersion)
throw RuntimeException("database produced by future version of the application")
begin()
for (v in (currentVersion + 1)..newVersion) {
val sv = if (v < 10) "00$v" else if (v < 100) "0$v" else "$v"
val filename = "migrations/$sv.sql"
val migrationFile = fileOpener.openResourceFile(filename)
for (line in migrationFile.lines()) {
if (line.isEmpty()) continue
run(line)
}
setVersion(v)
}
commit()
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.io
import org.isoron.platform.gui.*
interface FileOpener {
/**
* Opens a file which was shipped bundled with the application, such as a
* migration file.
*
* The path is relative to the assets folder. For example, to open
* assets/main/migrations/09.sql you should provide migrations/09.sql
* as the path.
*
* This function always succeed, even if the file does not exist.
*/
fun openResourceFile(path: String): ResourceFile
/**
* Opens a file which was not shipped with the application, such as
* databases and logs.
*
* The path is relative to the user folder. For example, if the application
* stores the user data at /home/user/.loop/ and you wish to open the file
* /home/user/.loop/crash.log, you should provide crash.log as the path.
*
* This function always succeed, even if the file does not exist.
*/
fun openUserFile(path: 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.
*/
interface UserFile {
/**
* Deletes the user file. If the file does not exist, nothing happens.
*/
suspend fun delete()
/**
* Returns true if the file exists.
*/
suspend fun exists(): Boolean
/**
* Returns the lines of the file. If the file does not exist, throws an
* exception.
*/
suspend fun lines(): List<String>
}
/**
* Represents a file that was shipped with the application, such as migration
* files or database templates.
*/
interface ResourceFile {
/**
* Copies the resource file to the specified user file. If the user file
* already exists, it is replaced. If not, a new file is created.
*/
suspend fun copyTo(dest: UserFile)
/**
* Returns the lines of the resource file. If the file does not exist,
* throws an exception.
*/
suspend fun lines(): List<String>
/**
* Returns true if the file exists.
*/
suspend fun exists(): Boolean
/**
* Loads resource file as an image.
*/
suspend fun toImage(): Image
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.io
interface Log {
fun info(tag: String, msg: String)
fun debug(tag: String, msg: String)
fun warn(tag: String, msg: String)
}
/**
* A Log that prints to the standard output.
*/
class StandardLog : Log {
override fun warn(tag: String, msg: String) {
val ftag = format("%-20s", tag)
println("W $ftag $msg")
}
override fun info(tag: String, msg: String) {
val ftag = format("%-20s", tag)
println("I $ftag $msg")
}
override fun debug(tag: String, msg: String) {
val ftag = format("%-20s", tag)
println("D $ftag $msg")
}
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.io
expect fun format(format: String, arg: String): String
expect fun format(format: String, arg: Int): String
expect fun format(format: String, arg: Double): String

View File

@@ -0,0 +1,173 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.time
import 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 millisSince1970: Long) {
val localDate: LocalDate
get() {
val millisSince2000 = millisSince1970 - 946684800000
val daysSince2000 = millisSince2000 / 86400000
return LocalDate(daysSince2000.toInt())
}
}
data class LocalDate(val daysSince2000: Int) {
var yearCache = -1
var monthCache = -1
var dayCache = -1
// init {
// if (daysSince2000 < 0)
// throw IllegalArgumentException("$daysSince2000 < 0")
// }
constructor(year: Int, month: Int, day: Int) :
this(daysSince2000(year, month, day))
val dayOfWeek: DayOfWeek
get() {
return when (daysSince2000 % 7) {
0 -> DayOfWeek.SATURDAY
1 -> DayOfWeek.SUNDAY
2 -> DayOfWeek.MONDAY
3 -> DayOfWeek.TUESDAY
4 -> DayOfWeek.WEDNESDAY
5 -> DayOfWeek.THURSDAY
else -> DayOfWeek.FRIDAY
}
}
val timestamp: Timestamp
get() {
return Timestamp(946684800000 + daysSince2000.toLong() * 86400000)
}
val year: Int
get() {
if (yearCache < 0) updateYearMonthDayCache()
return yearCache
}
val month: Int
get() {
if (monthCache < 0) updateYearMonthDayCache()
return monthCache
}
val day: Int
get() {
if (dayCache < 0) updateYearMonthDayCache()
return dayCache
}
private fun updateYearMonthDayCache() {
var currYear = 2000
var currDay = 0
while (true) {
val currYearLength = if (isLeapYear(currYear)) 366 else 365
if (daysSince2000 < currDay + currYearLength) {
yearCache = currYear
break
} else {
currYear++
currDay += currYearLength
}
}
var currMonth = 1
val monthOffset = if (isLeapYear(currYear)) leapOffset else nonLeapOffset
while (true) {
if (daysSince2000 < currDay + monthOffset[currMonth]) {
monthCache = currMonth
break
} else {
currMonth++
}
}
currDay += monthOffset[currMonth - 1]
dayCache = daysSince2000 - currDay + 1
}
fun isOlderThan(other: LocalDate): Boolean {
return daysSince2000 < other.daysSince2000
}
fun isNewerThan(other: LocalDate): Boolean {
return daysSince2000 > other.daysSince2000
}
fun plus(days: Int): LocalDate {
return LocalDate(daysSince2000 + days)
}
fun minus(days: Int): LocalDate {
return LocalDate(daysSince2000 - days)
}
fun distanceTo(other: LocalDate): Int {
return abs(daysSince2000 - other.daysSince2000)
}
}
interface LocalDateFormatter {
fun shortWeekdayName(date: LocalDate): String
fun shortMonthName(date: LocalDate): String
}
private fun isLeapYear(year: Int): Boolean {
return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
val leapOffset = arrayOf(0, 31, 60, 91, 121, 152, 182,
213, 244, 274, 305, 335, 366)
val nonLeapOffset = arrayOf(0, 31, 59, 90, 120, 151, 181,
212, 243, 273, 304, 334, 365)
private fun daysSince2000(year: Int, month: Int, day: Int): Int {
var result = 365 * (year - 2000)
result += ceil((year - 2000) / 4.0).toInt()
result -= ceil((year - 2000) / 100.0).toInt()
result += ceil((year - 2000) / 400.0).toInt()
if (isLeapYear(year)) {
result += leapOffset[month - 1]
} else {
result += nonLeapOffset[month - 1]
}
result += (day - 1)
return result
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits
const val LOOP_DATABASE_VERSION = 23

View File

@@ -0,0 +1,136 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.backend
import kotlinx.coroutines.*
import org.isoron.platform.concurrency.*
import org.isoron.platform.io.*
import org.isoron.uhabits.*
import org.isoron.uhabits.components.*
import org.isoron.uhabits.i18n.*
import org.isoron.uhabits.models.*
import kotlin.coroutines.*
open class BackendScope(private val ctx: CoroutineContext,
private val log: Log) : CoroutineScope {
private val job = Job()
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
log.info("Coroutine", throwable.toString())
}
override val coroutineContext: CoroutineContext
get() = ctx + job + exceptionHandler
}
class Backend(private val databaseName: String,
private val databaseOpener: DatabaseOpener,
private val fileOpener: FileOpener,
private val localeHelper: LocaleHelper,
private val log: Log,
private val scope: CoroutineContext
) : CoroutineScope by BackendScope(scope, log) {
private lateinit var database: Database
private lateinit var habitsRepository: HabitRepository
private lateinit var checkmarkRepository: CheckmarkRepository
lateinit var preferences: Preferences
lateinit var mainScreenDataSource: MainScreenDataSource
private val habits = mutableMapOf<Int, Habit>()
private val checkmarks = mutableMapOf<Habit, CheckmarkList>()
private val scores = mutableMapOf<Habit, ScoreList>()
var strings = localeHelper.getStringsForCurrentLocale()
var theme: Theme = LightTheme()
val observable = Observable<Listener>()
fun init() {
launch {
initDatabase()
initRepositories()
initDataSources()
observable.notifyListeners { it.onReady() }
}
}
private fun initRepositories() {
preferences = Preferences(PreferencesRepository(database))
habitsRepository = HabitRepository(database)
checkmarkRepository = CheckmarkRepository(database)
habits.putAll(habitsRepository.findAll())
log.info("Backend", "${habits.size} habits loaded")
for ((key, habit) in habits) {
val checks = checkmarkRepository.findAll(key)
checkmarks[habit] = CheckmarkList(habit.frequency, habit.type)
checkmarks[habit]?.setManualCheckmarks(checks)
scores[habit] = ScoreList(checkmarks[habit]!!)
}
}
private fun initDataSources() {
mainScreenDataSource =
MainScreenDataSource(preferences, habits, checkmarks, scores)
}
private suspend fun initDatabase() {
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)
}
fun createHabit(habit: Habit) {
val id = habitsRepository.nextId()
habit.id = id
habit.position = habits.size
habits[id] = habit
checkmarks[habit] = CheckmarkList(habit.frequency, habit.type)
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)
}
}
interface Listener {
fun onReady()
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.backend
import org.isoron.platform.concurrency.*
import org.isoron.platform.time.*
import org.isoron.uhabits.models.*
import org.isoron.uhabits.models.Checkmark.Companion.UNCHECKED
class MainScreenDataSource(val preferences: Preferences,
val habits: MutableMap<Int, Habit>,
val checkmarks: MutableMap<Habit, CheckmarkList>,
val scores: MutableMap<Habit, ScoreList>) {
val maxNumberOfButtons = 60
private val today = LocalDate(2019, 3, 30) /* TODO */
data class Data(val habits: List<Habit>,
val scores: Map<Habit, Score>,
val checkmarks: Map<Habit, List<Checkmark>>)
val observable = Observable<Listener>()
interface Listener {
fun onDataChanged(newData: Data)
}
fun requestData() {
var filtered = habits.values.toList()
if (!preferences.showArchived) {
filtered = filtered.filter { !it.isArchived }
}
val checkmarks = filtered.associate { habit ->
val allValues = checkmarks.getValue(habit).getUntil(today)
if (allValues.size <= maxNumberOfButtons) habit to allValues
else habit to allValues.subList(0, maxNumberOfButtons)
}
if (!preferences.showCompleted) {
filtered = filtered.filter { habit ->
(habit.type == HabitType.BOOLEAN_HABIT && checkmarks.getValue(habit)[0].value == UNCHECKED) ||
(habit.type == HabitType.NUMERICAL_HABIT && checkmarks.getValue(habit)[0].value * 1000 < habit.target)
}
}
val scores = filtered.associate { habit ->
habit to scores[habit]!!.getAt(today)
}
observable.notifyListeners { listener ->
val data = Data(filtered, scores, checkmarks)
listener.onDataChanged(data)
}
}
}

View File

@@ -0,0 +1,164 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.components
import org.isoron.platform.gui.*
import org.isoron.platform.time.*
import kotlin.math.*
class BarChart(var theme: Theme,
var dateFormatter: LocalDateFormatter) : Component {
// Data
var series = mutableListOf<List<Double>>()
var colors = mutableListOf<Color>()
var axis = listOf<LocalDate>()
// Style
var paddingTop = 20.0
var paddingLeft = 5.0
var paddingRight = 5.0
var footerHeight = 40.0
var barGroupMargin = 4.0
var barMargin = 4.0
var barWidth = 20.0
var nGridlines = 6
var backgroundColor = theme.cardBackgroundColor
override fun draw(canvas: Canvas) {
val width = canvas.getWidth()
val height = canvas.getHeight()
val n = series.size
val barGroupWidth = 2 * barGroupMargin + n * (barWidth + 2 * barMargin)
val safeWidth = width - paddingLeft - paddingRight
val nColumns = floor((safeWidth) / barGroupWidth).toInt()
val marginLeft = (safeWidth - nColumns * barGroupWidth) / 2
val maxBarHeight = height - footerHeight - paddingTop
var maxValue = series.map { it.max()!! }.max()!!
maxValue = max(maxValue, 1.0)
canvas.setColor(backgroundColor)
canvas.fillRect(0.0, 0.0, width, height)
fun barGroupOffset(c: Int) = marginLeft + paddingLeft +
(c) * barGroupWidth
fun barOffset(c: Int, s: Int) = barGroupOffset(c) +
barGroupMargin +
s * (barWidth + 2 * barMargin) +
barMargin
fun drawColumn(s: Int, c: Int) {
val value = if (c < series[s].size) series[s][c] else 0.0
val perc = value / maxValue
val barColorPerc = if (n > 1) 1.0 else round(perc / 0.20) * 0.20
val barColor = theme.lowContrastTextColor.blendWith(colors[s],
barColorPerc)
val barHeight = round(maxBarHeight * perc)
val x = barOffset(c, s)
val y = height - footerHeight - barHeight
canvas.setColor(barColor)
val r = round(barWidth * 0.33)
canvas.fillRect(x, y + r, barWidth, barHeight - r)
canvas.fillRect(x + r, y, barWidth - 2 * r, r)
canvas.fillCircle(x + r, y + r, r)
canvas.fillCircle(x + barWidth - r, y + r, r)
canvas.setFontSize(theme.smallTextSize)
canvas.setTextAlign(TextAlign.CENTER)
canvas.setColor(backgroundColor)
canvas.fillRect(x - barMargin,
y - theme.smallTextSize * 1.25,
barWidth + 2 * barMargin,
theme.smallTextSize * 1.0)
canvas.setColor(theme.mediumContrastTextColor)
canvas.drawText(value.toShortString(),
x + barWidth / 2,
y - theme.smallTextSize * 0.80)
}
fun drawSeries(s: Int) {
for (c in 0 until nColumns) drawColumn(s, c)
}
fun drawMajorGrid() {
canvas.setStrokeWidth(1.0)
if (n > 1) {
canvas.setColor(backgroundColor.blendWith(
theme.lowContrastTextColor,
0.5))
for (c in 0 until nColumns - 1) {
val x = barGroupOffset(c)
canvas.drawLine(x, paddingTop, x, paddingTop + maxBarHeight)
}
}
for (k in 1 until nGridlines) {
val pct = 1.0 - (k.toDouble() / (nGridlines - 1))
val y = paddingTop + maxBarHeight * pct
canvas.setColor(theme.lowContrastTextColor)
canvas.drawLine(0.0, y, width, y)
}
}
fun drawFooter() {
val y = paddingTop + maxBarHeight
canvas.setColor(backgroundColor)
canvas.fillRect(0.0, y, width, height - y)
canvas.setColor(theme.lowContrastTextColor)
canvas.drawLine(0.0, y, width, y)
canvas.setColor(theme.mediumContrastTextColor)
canvas.setTextAlign(TextAlign.CENTER)
var prevMonth = -1
var prevYear = -1
val isLargeInterval = (axis[0].distanceTo(axis[1]) > 300)
for (c in 0 until nColumns) {
val x = barGroupOffset(c)
val date = axis[c]
if(isLargeInterval) {
canvas.drawText(date.year.toString(),
x + barGroupWidth / 2,
y + theme.smallTextSize * 1.0)
} else {
if (date.month != prevMonth) {
canvas.drawText(dateFormatter.shortMonthName(date),
x + barGroupWidth / 2,
y + theme.smallTextSize * 1.0)
} else {
canvas.drawText(date.day.toString(),
x + barGroupWidth / 2,
y + theme.smallTextSize * 1.0)
}
if (date.year != prevYear) {
canvas.drawText(date.year.toString(),
x + barGroupWidth / 2,
y + theme.smallTextSize * 2.3)
}
}
prevMonth = date.month
prevYear = date.year
}
}
drawMajorGrid()
for (k in 0 until n) drawSeries(k)
drawFooter()
}
}

View File

@@ -0,0 +1,125 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.components
import org.isoron.platform.gui.*
import org.isoron.platform.time.*
import kotlin.math.*
class CalendarChart(var today: LocalDate,
var color: Color,
var theme: Theme,
var dateFormatter: LocalDateFormatter) : Component {
var padding = 5.0
var backgroundColor = Color(0xFFFFFF)
var squareSpacing = 1.0
var series = listOf<Double>()
var scrollPosition = 0
private var squareSize = 0.0
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 = today.dayOfWeek
val topLeftOffset = (nColumns - 1 + scrollPosition) * 7 + todayWeekday.index
val topLeftDate = today.minus(topLeftOffset)
repeat(nColumns) { column ->
val topOffset = topLeftOffset - 7 * column
val topDate = topLeftDate.plus(7 * column)
drawColumn(canvas, column, topDate, topOffset)
}
canvas.setColor(theme.mediumContrastTextColor)
repeat(7) { row ->
val date = topLeftDate.plus(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 = topDate.plus(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)
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.components
import org.isoron.platform.gui.*
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.setFontSize(theme.smallTextSize * 1.5)
canvas.setColor(when (value) {
2 -> color
else -> theme.lowContrastTextColor
})
val text = when (value) {
0 -> FontAwesome.TIMES
else -> FontAwesome.CHECK
}
canvas.drawText(text, canvas.getWidth() / 2.0, canvas.getHeight() / 2.0)
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.components
import org.isoron.platform.gui.*
import org.isoron.platform.time.*
class HabitListHeader(private val today: LocalDate,
private val nButtons: Int,
private val theme: Theme,
private val fmt: LocalDateFormatter) : Component {
override fun draw(canvas: Canvas) {
val width = canvas.getWidth()
val height = canvas.getHeight()
val buttonSize = theme.checkmarkButtonSize
canvas.setColor(theme.headerBackgroundColor)
canvas.fillRect(0.0, 0.0, width, height)
canvas.setColor(theme.headerBorderColor)
canvas.setStrokeWidth(0.5)
canvas.drawLine(0.0, height - 0.5, width, height - 0.5)
canvas.setColor(theme.headerTextColor)
canvas.setFont(Font.BOLD)
canvas.setFontSize(theme.smallTextSize)
repeat(nButtons) { index ->
val date = today.minus(nButtons - index - 1)
val name = fmt.shortWeekdayName(date).toUpperCase()
val number = date.day.toString()
val x = width - (index + 1) * buttonSize + buttonSize / 2
val y = height / 2
canvas.drawText(name, x, y - theme.smallTextSize * 0.6)
canvas.drawText(number, x, y + theme.smallTextSize * 0.6)
}
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.components
import org.isoron.platform.gui.*
import org.isoron.platform.io.*
import kotlin.math.*
fun Double.toShortString(): String = when {
this >= 1e9 -> format("%.1fG", this / 1e9)
this >= 1e8 -> format("%.0fM", this / 1e6)
this >= 1e7 -> format("%.1fM", this / 1e6)
this >= 1e6 -> format("%.1fM", this / 1e6)
this >= 1e5 -> format("%.0fk", this / 1e3)
this >= 1e4 -> format("%.1fk", this / 1e3)
this >= 1e3 -> format("%.1fk", this / 1e3)
this >= 1e2 -> format("%.0f", this)
this >= 1e1 -> when {
round(this) == this -> format("%.0f", this)
else -> format("%.1f", this)
}
else -> when {
round(this) == this -> format("%.0f", this)
round(this * 10) == this * 10 -> format("%.1f", this)
else -> format("%.2f", this)
}
}
class NumberButton(val color: Color,
val value: Double,
val threshold: Double,
val units: String,
val theme: Theme) : Component {
override fun draw(canvas: Canvas) {
val width = canvas.getWidth()
val height = canvas.getHeight()
val em = theme.smallTextSize
canvas.setColor(when {
value >= threshold -> color
value >= 0.01 -> theme.mediumContrastTextColor
else -> theme.lowContrastTextColor
})
canvas.setFontSize(theme.regularTextSize)
canvas.setFont(Font.BOLD)
canvas.drawText(value.toShortString(), width / 2, height / 2 - 0.6 * em)
canvas.setFontSize(theme.smallTextSize)
canvas.setFont(Font.REGULAR)
canvas.drawText(units, width / 2, height / 2 + 0.6 * em)
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.components
import org.isoron.platform.gui.*
import org.isoron.platform.io.*
import kotlin.math.*
class Ring(val color: Color,
val percentage: Double,
val thickness: Double,
val radius: Double,
val theme: Theme,
val label: Boolean = false) : Component {
override fun draw(canvas: Canvas) {
val width = canvas.getWidth()
val height = canvas.getHeight()
val angle = 360.0 * max(0.0, min(360.0, percentage))
canvas.setColor(theme.lowContrastTextColor)
canvas.fillCircle(width/2, height/2, radius)
canvas.setColor(color)
canvas.fillArc(width/2, height/2, radius, 90.0, -angle)
canvas.setColor(theme.cardBackgroundColor)
canvas.fillCircle(width/2, height/2, radius - thickness)
if(label) {
canvas.setColor(color)
canvas.setFontSize(radius * 0.4)
canvas.drawText(format("%.0f%%", percentage * 100), width / 2, height / 2)
}
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.components
import org.isoron.platform.gui.*
abstract class Theme {
val toolbarColor = Color(0xffffff)
val lowContrastTextColor = Color(0xe0e0e0)
val mediumContrastTextColor = Color(0x808080)
val highContrastTextColor = Color(0x202020)
val cardBackgroundColor = Color(0xFFFFFF)
val appBackgroundColor = Color(0xf4f4f4)
val toolbarBackgroundColor = Color(0xf4f4f4)
val statusBarBackgroundColor = Color(0x333333)
val headerBackgroundColor = Color(0xeeeeee)
val headerBorderColor = Color(0xcccccc)
val headerTextColor = mediumContrastTextColor
val itemBackgroundColor = Color(0xffffff)
fun color(paletteIndex: Int): Color {
return when (paletteIndex) {
0 -> Color(0xD32F2F)
1 -> Color(0x512DA8)
2 -> Color(0xF57C00)
3 -> Color(0xFF8F00)
4 -> Color(0xF9A825)
5 -> Color(0xAFB42B)
6 -> Color(0x7CB342)
7 -> Color(0x388E3C)
8 -> Color(0x00897B)
9 -> Color(0x00ACC1)
10 -> Color(0x039BE5)
11 -> Color(0x1976D2)
12 -> Color(0x303F9F)
13 -> Color(0x5E35B1)
14 -> Color(0x8E24AA)
15 -> Color(0xD81B60)
16 -> Color(0x5D4037)
else -> Color(0x000000)
}
}
val checkmarkButtonSize = 48.0
val smallTextSize = 12.0
val regularTextSize = 17.0
}
class LightTheme : Theme()

View File

@@ -0,0 +1,24 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.i18n
interface LocaleHelper {
fun getStringsForCurrentLocale(): Strings
}

View File

@@ -0,0 +1,190 @@
package org.isoron.uhabits.i18n
@Suppress("PropertyName", "unused")
open class Strings() {
open val about = "About"
open val action = "Action"
open val action_settings = "Settings"
open val add_habit = "Add habit"
open val all_time = "All time"
open val any_day = "Any day of the week"
open val any_weekday = "Monday to Friday"
open val app_name = "Loop Habit Tracker"
open val archive = "Archive"
open val behavior = "Behavior"
open val best_streaks = "Best streaks"
open val bug_report_failed = "Failed to generate bug report."
open val by_color = "By color"
open val by_name = "By name"
open val by_score = "By score"
open val calendar = "Calendar"
open val cancel = "Cancel"
open val change_value = "Change value"
open val check = "Check"
open val checkmark = "Checkmark"
open val checkmark_stack_widget = "Checkmark Stack Widget"
open val clear = "Clear"
open val clear_label = "Clear"
open val color_picker_default_title = "Change color"
open val could_not_export = "Failed to export data."
open val could_not_import = "Failed to import data."
open val count = "Count"
open val create_habit = "Create habit"
open val create_stackview_widget_button = "StackView Widget For All Habits"
open val current_streaks = "Current streak"
open val custom_frequency = "Custom …"
open val customize_notification = "Customize notifications"
open val customize_notification_summary = "Change sound, vibration, light and other notification settings"
open val database_repaired = "Database repaired."
open val day = "Day"
open val days = "days"
open val delete = "Delete"
open val delete_habits = "Delete Habits"
open val delete_habits_message = "The habits will be permanently deleted. This action cannot be undone."
open val description_hint = "Question (Did you … today?)"
open val developers = "Developers"
open val discard = "Discard"
open val done_label = "Done"
open val download = "Download"
open val edit = "Edit"
open val edit_habit = "Edit habit"
open val every_day = "Every day"
open val every_week = "Every week"
open val every_x_days = "Every %d days"
open val every_x_months = "Every %d months"
open val every_x_weeks = "Every %d weeks"
open val example_question_boolean = "e.g. Did you exercise today?"
open val example_question_numerical = "e.g. How many steps did you walk today?"
open val example_units = "e.g. steps"
open val export = "Export"
open val export_as_csv_summary = "Generates files that can be opened by spreadsheet software such as Microsoft Excel or OpenOffice Calc. This file cannot be imported back."
open val export_full_backup = "Export full backup"
open val export_full_backup_summary = "Generates a file that contains all your data. This file can be imported back."
open val export_to_csv = "Export as CSV"
open val file_not_recognized = "File not recognized."
open val filter = "Filter"
open val five_times_per_week = "5 times per week"
open val frequency = "Frequency"
open val frequency_stack_widget = "Frequency Stack Widget"
open val full_backup_success = "Full backup successfully exported."
open val generate_bug_report = "Generate bug report"
open val habit = "Habit"
open val habit_not_found = "Habit deleted / not found"
open val habit_strength = "Habit strength"
open val habits_imported = "Habits imported successfully."
open val help = "Help & FAQ"
open val help_translate = "Help translate this app"
open val hide_archived = "Hide archived"
open val hide_completed = "Hide completed"
open val hint_drag = "To rearrange the entries, press-and-hold on the name of the habit, then drag it to the correct place."
open val hint_landscape = "You can see more days by putting your phone in landscape mode."
open val hint_title = "Did you know?"
open val history = "History"
open val history_stack_widget = "History Stack Widget"
open val import_data = "Import data"
open val import_data_summary = "Supports full backups exported by this app, as well as files generated by Tickmate, HabitBull or Rewire. See FAQ for more information."
open val interface_preferences = "Interface"
open val interval_15_minutes = "15 minutes"
open val interval_1_hour = "1 hour"
open val interval_24_hour = "24 hours"
open val interval_2_hour = "2 hours"
open val interval_30_minutes = "30 minutes"
open val interval_4_hour = "4 hours"
open val interval_8_hour = "8 hours"
open val interval_always_ask = "Always ask"
open val interval_custom = "Custom..."
open val intro_description_1 = "Loop Habit Tracker helps you create and maintain good habits."
open val intro_description_2 = "Every day, after performing your habit, put a checkmark on the app."
open val intro_description_3 = "Habits performed consistently for a long time will earn a full star."
open val intro_description_4 = "Detailed graphs show you how your habits improved over time."
open val intro_title_1 = "Welcome"
open val intro_title_2 = "Create some new habits"
open val intro_title_3 = "Keep doing it"
open val intro_title_4 = "Track your progress"
open val last_x_days = "Last %d days"
open val last_x_months = "Last %d months"
open val last_x_weeks = "Last %d weeks"
open val last_x_years = "Last %d years"
open val led_notifications = "Notification light"
open val led_notifications_description = "Shows a blinking light for reminders. Only available in phones with LED notification lights."
open val links = "Links"
open val long_press_to_edit = "Press-and-hold to change the value"
open val long_press_to_toggle = "Press-and-hold to check or uncheck"
open val main_activity_title = "Habits"
open val manually = "Manually"
open val month = "Month"
open val name = "Name"
open val night_mode = "Night mode"
open val no = "No"
open val no_habits_found = "You have no active habits"
open val none = "None"
open val number_of_repetitions = "Number of repetitions"
open val overview = "Overview"
open val pref_rate_this_app = "Rate this app on Google Play"
open val pref_send_feedback = "Send feedback to developer"
open val pref_snooze_interval_title = "Snooze interval on reminders"
open val pref_toggle_description = "Put checkmarks with a single tap instead of press-and-hold. More convenient, but might cause accidental toggles."
open val pref_toggle_title = "Toggle with short press"
open val pref_view_app_introduction = "View app introduction"
open val pref_view_source_code = "View source code at GitHub"
open val pure_black_description = "Replaces gray backgrounds with pure black in night mode. Reduces battery usage in phones with AMOLED display."
open val quarter = "Quarter"
open val question = "Question"
open val reminder = "Reminder"
open val reminder_off = "Off"
open val reminder_sound = "Reminder sound"
open val repair_database = "Repair database"
open val repeat = "Repeat"
open val reverse_days = "Reverse order of days"
open val reverse_days_description = "Show days in reverse order on the main screen."
open val save = "Save"
open val score = "Score"
open val score_stack_widget = "Score Stack Widget"
open val select_habit_requirement_prompt = "Please select at least one habit"
open val select_hours = "Select hours"
open val select_minutes = "Select minutes"
open val select_snooze_delay = "Select snooze delay"
open val select_weekdays = "Select days"
open val settings = "Settings"
open val show_archived = "Show archived"
open val show_completed = "Show completed"
open val snooze = "Later"
open val snooze_interval = "Snooze interval"
open val sort = "Sort"
open val sticky_notifications = "Make notifications sticky"
open val sticky_notifications_description = "Prevents notifications from being swiped away."
open val streaks = "Streaks"
open val streaks_stack_widget = "Streaks Stack Widget"
open val strength = "Strength"
open val target = "Target"
open val time_every = "time in"
open val times_every = "times in"
open val toast_habit_archived = "Habits archived"
open val toast_habit_changed = "Habit changed"
open val toast_habit_changed_back = "Habit changed back"
open val toast_habit_created = "Habit created"
open val toast_habit_deleted = "Habits deleted"
open val toast_habit_restored = "Habits restored"
open val toast_habit_unarchived = "Habits unarchived"
open val toast_nothing_to_redo = "Nothing to redo"
open val toast_nothing_to_undo = "Nothing to undo"
open val toggle = "Toggle"
open val total = "Total"
open val translators = "Translators"
open val troubleshooting = "Troubleshooting"
open val two_times_per_week = "2 times per week"
open val unarchive = "Unarchive"
open val uncheck = "Uncheck"
open val unit = "Unit"
open val use_pure_black = "Use pure black in night mode"
open val validation_at_most_one_rep_per_day = "You can have at most one repetition per day"
open val validation_name_should_not_be_blank = "Name cannot be blank."
open val validation_number_should_be_positive = "Number must be positive."
open val validation_show_not_be_blank = "This field should not be blank"
open val version_n = "Version %s"
open val week = "Week"
open val weekends = "Weekends"
open val year = "Year"
open val yes = "Yes"
open val day_mode = "Day mode"
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models
import org.isoron.platform.time.*
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
}
}

View File

@@ -0,0 +1,204 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models
import org.isoron.platform.time.*
import org.isoron.uhabits.models.Checkmark.Companion.CHECKED_AUTOMATIC
import org.isoron.uhabits.models.Checkmark.Companion.CHECKED_MANUAL
import org.isoron.uhabits.models.Checkmark.Companion.UNCHECKED
class CheckmarkList(val frequency: Frequency,
val habitType: HabitType) {
private val manualCheckmarks = mutableListOf<Checkmark>()
private val computedCheckmarks = mutableListOf<Checkmark>()
/**
* Replaces the entire list of manual checkmarks by the ones provided. The
* list of automatic checkmarks will be automatically updated.
*/
fun setManualCheckmarks(checks: List<Checkmark>) {
manualCheckmarks.clear()
computedCheckmarks.clear()
manualCheckmarks.addAll(checks)
if (habitType == HabitType.NUMERICAL_HABIT) {
computedCheckmarks.addAll(checks)
} else {
val computed = computeCheckmarks(checks, frequency)
computedCheckmarks.addAll(computed)
}
}
/**
* 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 getUntil(date: LocalDate): List<Checkmark> {
if (computedCheckmarks.isEmpty()) return listOf()
val result = mutableListOf<Checkmark>()
val newest = computedCheckmarks.first().date
val distToNewest = newest.distanceTo(date)
var k = 0
var fromIndex = 0
val toIndex = computedCheckmarks.size
if (newest.isOlderThan(date)) {
repeat(distToNewest) { result.add(Checkmark(date.minus(k++), UNCHECKED)) }
} else {
fromIndex = distToNewest
}
val subList = computedCheckmarks.subList(fromIndex, toIndex)
result.addAll(subList.map { Checkmark(date.minus(k++), it.value) })
return result
}
companion object {
/**
* Computes the list of automatic checkmarks a list of manual ones.
*/
fun computeCheckmarks(checks: List<Checkmark>,
frequency: Frequency
): MutableList<Checkmark> {
val intervals = buildIntervals(checks, frequency)
snapIntervalsTogether(intervals)
return buildCheckmarksFromIntervals(checks, intervals)
}
/**
* Modifies the intervals so that gaps between intervals are eliminated.
*
* More specifically, this function shifts the beginning and the end of
* intervals so that they overlap the least as possible. The center of
* the interval, however, still falls within the interval. The length of
* the intervals are also not modified.
*/
fun snapIntervalsTogether(intervals: MutableList<Interval>) {
for (i in 1 until intervals.size) {
val (begin, center, end) = intervals[i]
val (_, _, prevEnd) = intervals[i - 1]
val gap = prevEnd.distanceTo(begin) - 1
if (gap <= 0 || end.minus(gap).isOlderThan(center)) continue
intervals[i] = Interval(begin.minus(gap),
center,
end.minus(gap))
}
}
/**
* Converts a list of (manually checked) checkmarks and computed
* intervals into a list of unchecked, manually checked and
* automatically checked checkmarks.
*
* Manual checkmarks are simply copied over to the output list. Days
* that are an interval, but which do not have manual checkmarks receive
* automatic checkmarks. Days that fall in the gaps between intervals
* receive unchecked checkmarks.
*/
fun buildCheckmarksFromIntervals(checks: List<Checkmark>,
intervals: List<Interval>
): MutableList<Checkmark> {
if (checks.isEmpty()) throw IllegalArgumentException()
if (intervals.isEmpty()) throw IllegalArgumentException()
var oldest = intervals[0].begin
var newest = intervals[0].end
for (interval in intervals) {
if (interval.begin.isOlderThan(oldest)) oldest = interval.begin
if (interval.end.isNewerThan(newest)) newest = interval.end
}
for (check in checks) {
if (check.date.isOlderThan(oldest)) oldest = check.date
if (check.date.isNewerThan(newest)) newest = check.date
}
val distance = oldest.distanceTo(newest)
val checkmarks = mutableListOf<Checkmark>()
for (offset in 0..distance)
checkmarks.add(Checkmark(newest.minus(offset),
UNCHECKED))
for (interval in intervals) {
val beginOffset = newest.distanceTo(interval.begin)
val endOffset = newest.distanceTo(interval.end)
for (offset in endOffset..beginOffset) {
checkmarks.set(offset,
Checkmark(newest.minus(offset),
CHECKED_AUTOMATIC))
}
}
for (check in checks) {
val offset = newest.distanceTo(check.date)
checkmarks.set(offset, Checkmark(check.date, CHECKED_MANUAL))
}
return checkmarks
}
/**
* Constructs a list of intervals based on a list of (manual)
* checkmarks.
*/
fun buildIntervals(checks: List<Checkmark>,
frequency: Frequency): MutableList<Interval> {
val num = frequency.numerator
val den = frequency.denominator
val intervals = mutableListOf<Interval>()
for (i in 0..(checks.size - num)) {
val first = checks[i]
val last = checks[i + num - 1]
if (first.date.distanceTo(last.date) >= den) continue
val end = first.date.plus(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)
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models
import org.isoron.platform.io.*
import org.isoron.platform.time.*
class CheckmarkRepository(db: Database) {
private val findStatement = db.prepareStatement("select timestamp, value from Repetitions where habit = ? order by timestamp desc")
private val insertStatement = db.prepareStatement("insert into Repetitions(habit, timestamp, value) values (?, ?, ?)")
private val deleteStatement = db.prepareStatement("delete from Repetitions where habit=? and timestamp=?")
fun findAll(habitId: Int): List<Checkmark> {
findStatement.bindInt(0, habitId)
val result = mutableListOf<Checkmark>()
while (findStatement.step() == StepResult.ROW) {
val date = Timestamp(findStatement.getLong(0)).localDate
val value = findStatement.getInt(1)
result.add(Checkmark(date, value))
}
findStatement.reset()
return result
}
fun insert(habitId: Int, checkmark: Checkmark) {
val timestamp = checkmark.date.timestamp
insertStatement.bindInt(0, habitId)
insertStatement.bindLong(1, timestamp.millisSince1970)
insertStatement.bindInt(2, checkmark.value)
insertStatement.step()
insertStatement.reset()
}
fun delete(habitId: Int, date: LocalDate) {
val timestamp = date.timestamp
deleteStatement.bindInt(0, habitId)
deleteStatement.bindLong(1, timestamp.millisSince1970)
deleteStatement.step()
deleteStatement.reset()
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models
data class Frequency(val numerator: Int,
val denominator: Int) {
fun toDouble(): Double {
return numerator.toDouble() / denominator
}
companion object {
val WEEKLY = Frequency(1, 7)
val DAILY = Frequency(1, 1)
val TWO_TIMES_PER_WEEK = Frequency(2, 7)
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models
import org.isoron.platform.gui.*
data class Habit(var id: Int,
var name: String,
var description: String,
var frequency: Frequency,
var color: PaletteColor,
var isArchived: Boolean,
var position: Int,
var unit: String,
var target: Double,
var type: HabitType)

View File

@@ -0,0 +1,100 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models
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) {
companion object {
const val SELECT_COLUMNS = "id, name, description, freq_num, freq_den, color, archived, position, unit, target_value, type"
const val SELECT_PLACEHOLDERS = "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?"
const val UPDATE_COLUMNS = "id=?, name=?, description=?, freq_num=?, freq_den=?, color=?, archived=?, position=?, unit=?, target_value=?, type=?"
}
private val findAllStatement = db.prepareStatement("select $SELECT_COLUMNS from habits order by position")
private val insertStatement = db.prepareStatement("insert into Habits($SELECT_COLUMNS) values ($SELECT_PLACEHOLDERS)")
private val updateStatement = db.prepareStatement("update Habits set $UPDATE_COLUMNS where id=?")
private val deleteStatement = db.prepareStatement("delete from Habits where id=?")
fun nextId(): Int {
return db.nextId("Habits")
}
fun findAll(): MutableMap<Int, Habit> {
val result = mutableMapOf<Int, Habit>()
while (findAllStatement.step() == StepResult.ROW) {
val habit = buildHabitFromStatement(findAllStatement)
result[habit.id] = habit
}
findAllStatement.reset()
return result
}
fun insert(habit: Habit) {
bindHabitToStatement(habit, insertStatement)
insertStatement.step()
insertStatement.reset()
}
fun update(habit: Habit) {
bindHabitToStatement(habit, updateStatement)
updateStatement.bindInt(11, habit.id)
updateStatement.step()
updateStatement.reset()
}
private fun buildHabitFromStatement(stmt: PreparedStatement): Habit {
return Habit(id = stmt.getInt(0),
name = stmt.getText(1),
description = stmt.getText(2),
frequency = Frequency(stmt.getInt(3), stmt.getInt(4)),
color = PaletteColor(stmt.getInt(5)),
isArchived = stmt.getInt(6) != 0,
position = stmt.getInt(7),
unit = stmt.getText(8),
target = stmt.getReal(9),
type = if (stmt.getInt(10) == 0) HabitType.BOOLEAN_HABIT else HabitType.NUMERICAL_HABIT)
}
private fun bindHabitToStatement(habit: Habit, statement: PreparedStatement) {
statement.bindInt(0, habit.id)
statement.bindText(1, habit.name)
statement.bindText(2, habit.description)
statement.bindInt(3, habit.frequency.numerator)
statement.bindInt(4, habit.frequency.denominator)
statement.bindInt(5, habit.color.index)
statement.bindInt(6, if (habit.isArchived) 1 else 0)
statement.bindInt(7, habit.position)
statement.bindText(8, habit.unit)
statement.bindReal(9, habit.target)
statement.bindInt(10, habit.type.code)
}
fun delete(habit: Habit) {
deleteStatement.bindInt(0, habit.id)
deleteStatement.step()
deleteStatement.reset()
}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models
enum class HabitType(val code: Int) {
BOOLEAN_HABIT(0),
NUMERICAL_HABIT(1),
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models
class Preferences(private val repository: PreferencesRepository) {
var showArchived = repository.getBoolean("show_archived", false)
set(value) {
repository.putBoolean("show_archived", value)
field = value
}
var showCompleted = repository.getBoolean("show_completed", true)
set(value) {
repository.putBoolean("show_completed", value)
field = value
}
var nightMode = repository.getBoolean("night_mode", false)
set(value) {
repository.putBoolean("night_mode", value)
field = value
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models
import org.isoron.platform.io.*
class PreferencesRepository(private val db: Database) {
private val insertStatement = db.prepareStatement("insert into Preferences(key, value) values (?, ?)")
private val deleteStatement = db.prepareStatement("delete from Preferences where key=?")
private val selectStatement = db.prepareStatement("select value from Preferences where key=?")
fun putBoolean(key: String, value: Boolean) {
putString(key, value.toString())
}
fun getBoolean(key: String, default: Boolean): Boolean {
val value = getString(key, "NULL")
return if (value == "NULL") default else value.toBoolean()
}
fun putLong(key: String, value: Long) {
putString(key, value.toString())
}
fun getLong(key: String, default: Long): Long {
val value = getString(key, "NULL")
return if (value == "NULL") default else value.toLong()
}
fun putString(key: String, value: String) {
deleteStatement.bindText(0, key)
deleteStatement.step()
deleteStatement.reset()
insertStatement.bindText(0, key)
insertStatement.bindText(1, value)
insertStatement.step()
insertStatement.reset()
}
fun getString(key: String, default: String): String {
selectStatement.bindText(0, key)
if (selectStatement.step() == StepResult.DONE) {
selectStatement.reset()
return default
} else {
val value = selectStatement.getText(0)
selectStatement.reset()
return value
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models
import org.isoron.platform.time.*
/**
* A Score is a number which indicates how strong the habit is at a given date.
*
* Scores are computed by taking an exponential moving average of the values of
* the checkmarks in preceding days. For boolean habits, when computing the
* average, each checked day (whether the check was manual or automatic) has
* value as 1, while days without checkmarks have value 0.
*
* For numerical habits, each day that exceeded the target has value 1, while
* days which failed to exceed the target receive a partial value, based on the
* proportion that was completed. For example, if the target is 100 units and
* the user completed 70 units, then the value for that day is 0.7 when
* computing the average.
*/
data class Score(val date: LocalDate,
val value: Double)

View File

@@ -0,0 +1,70 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models
import org.isoron.platform.time.*
import kotlin.math.*
class ScoreList(private val checkmarkList: CheckmarkList) {
/**
* Returns a list of all scores, from the beginning of the habit history
* until the specified date.
*
* 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 getUntil(date: LocalDate): List<Score> {
val frequency = checkmarkList.frequency
val checks = checkmarkList.getUntil(date)
val scores = mutableListOf<Score>()
val type = checkmarkList.habitType
var currentScore = 0.0
checks.reversed().forEach { check ->
val value = if (type == HabitType.BOOLEAN_HABIT) {
min(1, check.value)
} else {
check.value
}
currentScore = compute(frequency, currentScore, value)
scores.add(Score(check.date, currentScore))
}
return scores.reversed()
}
fun getAt(date: LocalDate): Score {
return getUntil(date)[0]
}
companion object {
/**
* Given the frequency of the habit, the previous score, and the value of
* the current checkmark, computes the current score for the habit.
*/
fun compute(frequency: Frequency,
previousScore: Double,
checkmarkValue: Int): Double {
val multiplier = 0.5.pow(frequency.toDouble() / 13.0)
val score = previousScore * multiplier + checkmarkValue * (1 - multiplier)
return floor(score * 1e6) / 1e6
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models
import org.isoron.platform.time.*
/**
* A streak is an uninterrupted sequence of days where the habit was performed.
*
* For daily boolean habits, the definition is straightforward: a streak is a
* sequence of days that have checkmarks. For non-daily habits, note
* that automatic checkmarks (the ones added by the app) can also keep the
* streak going. For numerical habits, a streak is a sequence of days where the
* user has consistently exceeded the target for the habit.
*/
data class Streak(val start: LocalDate,
val end: LocalDate)

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models
class StreakList(private val checkmarkList: CheckmarkList) {
/**
* Returns the longest streaks.
*
* The argument specifies the maximum number of streaks to find. The
* returned list is sorted by date (descending). That is, the first element
* corresponds to the most recent streak.
*/
fun getBest(limit: Int): List<Streak> {
TODO()
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.concurrency
import kotlinx.coroutines.*
import platform.darwin.*
import kotlin.coroutines.*
class UIDispatcher : CoroutineDispatcher() {
override fun dispatch(context: CoroutineContext, block: Runnable) {
val queue = dispatch_get_main_queue()
dispatch_async(queue) {
block.run()
}
}
}

View File

@@ -0,0 +1,149 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.gui
import kotlinx.cinterop.*
import platform.CoreGraphics.*
import platform.Foundation.*
import platform.UIKit.*
import kotlin.math.*
val Color.uicolor: UIColor
get() = UIColor.colorWithRed(this.red, this.green, this.blue, this.alpha)
val Color.cgcolor: CGColorRef?
get() = uicolor.CGColor
class IosCanvas(val width: Double,
val height: Double,
val scale: Double = 2.0
) : Canvas {
var textColor = UIColor.blackColor
var font = Font.REGULAR
var fontSize = 12.0
var textAlign = TextAlign.CENTER
val ctx = UIGraphicsGetCurrentContext()!!
override fun setColor(color: Color) {
CGContextSetStrokeColorWithColor(ctx, color.cgcolor)
CGContextSetFillColorWithColor(ctx, color.cgcolor)
textColor = color.uicolor
}
override fun drawLine(x1: Double, y1: Double, x2: Double, y2: Double) {
CGContextMoveToPoint(ctx, x1 * scale, y1 * scale)
CGContextAddLineToPoint(ctx, x2 * scale, y2 * scale)
CGContextStrokePath(ctx)
}
@Suppress("CAST_NEVER_SUCCEEDS")
override fun drawText(text: String, x: Double, y: Double) {
val sx = scale * x
val sy = scale * y
val nsText = (text as NSString)
val uiFont = when (font) {
Font.REGULAR -> UIFont.systemFontOfSize(fontSize)
Font.BOLD -> UIFont.boldSystemFontOfSize(fontSize)
Font.FONT_AWESOME -> UIFont.fontWithName("FontAwesome", fontSize)
}
val size = nsText.sizeWithFont(uiFont)
val width = size.useContents { width }
val height = size.useContents { height }
val origin = when (textAlign) {
TextAlign.CENTER -> CGPointMake(sx - width / 2, sy - height / 2)
TextAlign.LEFT -> CGPointMake(sx, sy - height / 2)
TextAlign.RIGHT -> CGPointMake(sx - width, sy - height / 2)
}
nsText.drawAtPoint(origin, uiFont)
}
override fun fillRect(x: Double, y: Double, width: Double, height: Double) {
CGContextFillRect(ctx,
CGRectMake(x * scale,
y * scale,
width * scale,
height * scale))
}
override fun drawRect(x: Double, y: Double, width: Double, height: Double) {
CGContextStrokeRect(ctx,
CGRectMake(x * scale,
y * scale,
width * scale,
height * scale))
}
override fun getHeight(): Double {
return height
}
override fun getWidth(): Double {
return width
}
override fun setFont(font: Font) {
this.font = font
}
override fun setFontSize(size: Double) {
this.fontSize = size * scale
}
override fun setStrokeWidth(size: Double) {
CGContextSetLineWidth(ctx, size * scale)
}
override fun fillArc(centerX: Double,
centerY: Double,
radius: Double,
startAngle: Double,
swipeAngle: Double) {
val a1 = startAngle / 180 * PI * (-1)
val a2 = a1 - swipeAngle / 180 * PI
CGContextBeginPath(ctx)
CGContextMoveToPoint(ctx, centerX * scale, centerY * scale)
CGContextAddArc(ctx,
centerX * scale,
centerY * scale,
radius * scale,
a1,
a2,
if (swipeAngle > 0) 1 else 0)
CGContextClosePath(ctx)
CGContextFillPath(ctx)
}
override fun fillCircle(centerX: Double, centerY: Double, radius: Double) {
val rect = CGRectMake(scale * (centerX - radius),
scale * (centerY - radius),
scale * radius * 2.0,
scale * radius * 2.0)
CGContextFillEllipseInRect(ctx, rect)
}
override fun setTextAlign(align: TextAlign) {
this.textAlign = align
}
override fun toImage(): Image {
return IosImage(UIGraphicsGetImageFromCurrentImageContext()!!)
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.gui
import platform.UIKit.*
import platform.CoreGraphics.*
import platform.Foundation.*
class IosImage(val image: UIImage) : Image {
override val width: Int
get() {
return CGImageGetWidth(image.CGImage).toInt()
}
override val height: Int
get() {
return CGImageGetHeight(image.CGImage).toInt()
}
override fun getPixel(x: Int, y: Int): Color {
return Color(1.0, 0.0, 0.0, 1.0)
}
override fun setPixel(x: Int, y: Int, color: Color) {
}
@Suppress("CAST_NEVER_SUCCEEDS")
override suspend fun export(path: String) {
val tmpPath = "${NSTemporaryDirectory()}/$path"
val dir = (tmpPath as NSString).stringByDeletingLastPathComponent
NSFileManager.defaultManager.createDirectoryAtPath(dir, true, null, null)
val data = UIImagePNGRepresentation(image)!!
val success = data.writeToFile(tmpPath, true)
if (!success) throw RuntimeException("could not write to $tmpPath")
println(tmpPath)
}
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.io
import kotlinx.cinterop.*
import platform.Foundation.*
import sqlite3.*
fun sqlite3_errstr(db: CPointer<sqlite3>): String {
return "SQLite3 error: " + sqlite3_errmsg(db).toString()
}
@Suppress("CAST_NEVER_SUCCEEDS")
class IosDatabaseOpener : DatabaseOpener {
override fun open(file: UserFile): Database = memScoped {
val path = (file as IosFile).path
val dirname = (path as NSString).stringByDeletingLastPathComponent
NSFileManager.defaultManager.createDirectoryAtPath(dirname, true, null, null)
val db = alloc<CPointerVar<sqlite3>>()
val result = sqlite3_open(path, db.ptr)
if (result != SQLITE_OK)
throw Exception("sqlite3_open failed (code $result)")
return IosDatabase(db.value!!)
}
}
class IosDatabase(val db: CPointer<sqlite3>) : Database {
override fun prepareStatement(sql: String): PreparedStatement = memScoped {
if (sql.isEmpty()) throw Exception("empty SQL query")
val stmt = alloc<CPointerVar<sqlite3_stmt>>()
val result = sqlite3_prepare_v2(db, sql.cstr, -1, stmt.ptr, null)
if (result != SQLITE_OK)
throw Exception("sqlite3_prepare_v2 failed (code $result)")
return IosPreparedStatement(db, stmt.value!!)
}
override fun close() {
sqlite3_close(db)
}
}
class IosPreparedStatement(val db: CPointer<sqlite3>,
val stmt: CPointer<sqlite3_stmt>) : PreparedStatement {
override fun step(): StepResult {
when (sqlite3_step(stmt)) {
SQLITE_ROW -> return StepResult.ROW
SQLITE_DONE -> return StepResult.DONE
else -> throw Exception(sqlite3_errstr(db))
}
}
override fun finalize() {
sqlite3_finalize(stmt)
}
override fun getInt(index: Int): Int {
return sqlite3_column_int(stmt, index)
}
override fun getLong(index: Int): Long {
return sqlite3_column_int64(stmt, index)
}
override fun getText(index: Int): String {
return sqlite3_column_text(stmt, index)!!
.reinterpret<ByteVar>()
.toKString()
}
override fun getReal(index: Int): Double {
return sqlite3_column_double(stmt, index)
}
override fun bindInt(index: Int, value: Int) {
sqlite3_bind_int(stmt, index + 1, value)
}
override fun bindLong(index: Int, value: Long) {
sqlite3_bind_int64(stmt, index + 1, value)
}
override fun bindText(index: Int, value: String) {
sqlite3_bind_text(stmt, index + 1, value, -1, SQLITE_TRANSIENT)
}
override fun bindReal(index: Int, value: Double) {
sqlite3_bind_double(stmt, index + 1, value)
}
override fun reset() {
sqlite3_reset(stmt)
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.io
import org.isoron.platform.gui.*
import platform.Foundation.*
import platform.UIKit.*
class IosFileOpener : FileOpener {
override fun openResourceFile(path: String): ResourceFile {
val resPath = NSBundle.mainBundle.resourcePath!!
return IosFile("$resPath/$path")
}
override fun openUserFile(path: String): UserFile {
val manager = NSFileManager.defaultManager
val basePath = manager.URLsForDirectory(NSDocumentDirectory, NSUserDomainMask)
val filePath = (basePath.first() as NSURL).URLByAppendingPathComponent(path)!!.path!!
return IosFile(filePath)
}
}
class IosFile(val path: String) : UserFile, ResourceFile {
override suspend fun delete() {
NSFileManager.defaultManager.removeItemAtPath(path, null)
}
override suspend fun exists(): Boolean {
return NSFileManager.defaultManager.fileExistsAtPath(path)
}
override suspend fun lines(): List<String> {
if (!exists()) throw Exception("File not found: $path")
val contents = NSString.stringWithContentsOfFile(path)
return contents.toString().lines()
}
@Suppress("CAST_NEVER_SUCCEEDS")
override suspend fun copyTo(dest: UserFile) {
val destPath = (dest as IosFile).path
val manager = NSFileManager.defaultManager
val destParentPath = (destPath as NSString).stringByDeletingLastPathComponent
NSFileManager.defaultManager.createDirectoryAtPath(destParentPath, true, null, null)
manager.copyItemAtPath(path, destPath, null)
}
override suspend fun toImage(): Image {
return IosImage(UIImage.imageWithContentsOfFile(path)!!)
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.io
import kotlinx.cinterop.*
// Although the three following methods have exactly the same implementation,
// replacing them all by a single format(format: String, arg: Any) breaks
// everything, as of Kotlin/Native 1.3.72. Apparently, Kotlin/Native is not
// able to do proper type conversions for variables of type Any when calling
// C functions.
actual fun format(format: String, arg: String): String {
val buffer = ByteArray(1000)
buffer.usePinned { p -> platform.posix.sprintf(p.addressOf(0), format, arg) }
return buffer.toKString()
}
actual fun format(format: String, arg: Int): String {
val buffer = ByteArray(1000)
buffer.usePinned { p -> platform.posix.sprintf(p.addressOf(0), format, arg) }
return buffer.toKString()
}
actual fun format(format: String, arg: Double): String {
val buffer = ByteArray(1000)
buffer.usePinned { p -> platform.posix.sprintf(p.addressOf(0), format, arg) }
return buffer.toKString()
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.time
import platform.Foundation.*
fun LocalDate.toNsDate(): NSDate {
val calendar = NSCalendar.calendarWithIdentifier(NSCalendarIdentifierGregorian)!!
val dc = NSDateComponents()
dc.year = year.toLong()
dc.month = month.toLong()
dc.day = day.toLong()
dc.hour = 13
dc.minute = 0
return calendar.dateFromComponents(dc)!!
}
class IosLocalDateFormatter(val locale: String) : LocalDateFormatter {
constructor() : this(NSLocale.preferredLanguages[0] as String)
private val fmt = NSDateFormatter()
init {
fmt.setLocale(NSLocale.localeWithLocaleIdentifier(locale))
}
override fun shortWeekdayName(date: LocalDate): String {
fmt.dateFormat = "EEE"
return fmt.stringFromDate(date.toNsDate())
}
override fun shortMonthName(date: LocalDate): String {
fmt.dateFormat = "MMM"
return fmt.stringFromDate(date.toNsDate())
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@file:Suppress("UNCHECKED_CAST")
package org.isoron.uhabits
import org.isoron.platform.io.*
import org.isoron.uhabits.i18n.*
import platform.Foundation.*
class IosLocaleHelper(private val log: Log) : LocaleHelper {
override fun getStringsForCurrentLocale(): Strings {
val pref = NSLocale.preferredLanguages as List<String>
val lang = if (pref.isEmpty()) "en-US" else pref[0]
log.info("IosLocaleHelper", lang)
return when {
else -> Strings()
}
}
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.gui
import kotlinx.coroutines.*
import org.w3c.dom.*
import kotlin.js.*
import kotlin.math.*
class JsCanvas(val element: HTMLCanvasElement,
val pixelScale: Double) : Canvas {
val ctx = element.getContext("2d") as CanvasRenderingContext2D
var fontSize = 12.0
var fontFamily = "NotoRegular"
var align = CanvasTextAlign.CENTER
private fun toPixel(x: Double): Double {
return pixelScale * x
}
private fun toDp(x: Int): Double {
return x / pixelScale
}
override fun setColor(color: Color) {
val c = "rgb(${color.red * 255}, ${color.green * 255}, ${color.blue * 255})"
ctx.fillStyle = c;
ctx.strokeStyle = c;
}
override fun drawLine(x1: Double, y1: Double, x2: Double, y2: Double) {
ctx.beginPath()
ctx.moveTo(toPixel(x1), toPixel(y1))
ctx.lineTo(toPixel(x2), toPixel(y2))
ctx.stroke()
}
override fun drawText(text: String, x: Double, y: Double) {
ctx.font = "${fontSize}px ${fontFamily}"
ctx.textAlign = align
ctx.textBaseline = CanvasTextBaseline.MIDDLE
ctx.fillText(text, toPixel(x), toPixel(y + fontSize * 0.025))
}
override fun fillRect(x: Double, y: Double, width: Double, height: Double) {
ctx.fillRect(toPixel(x),
toPixel(y),
toPixel(width),
toPixel(height))
}
override fun drawRect(x: Double, y: Double, width: Double, height: Double) {
ctx.strokeRect(toPixel(x),
toPixel(y),
toPixel(width),
toPixel(height))
}
override fun getHeight(): Double {
return toDp(element.height)
}
override fun getWidth(): Double {
return toDp(element.width)
}
override fun setFont(font: Font) {
fontFamily = when (font) {
Font.REGULAR -> "NotoRegular"
Font.BOLD -> "NotoBold"
Font.FONT_AWESOME -> "FontAwesome"
}
}
override fun setFontSize(size: Double) {
fontSize = size * pixelScale
}
override fun setStrokeWidth(size: Double) {
ctx.lineWidth = size * pixelScale
}
override fun fillArc(centerX: Double,
centerY: Double,
radius: Double,
startAngle: Double,
swipeAngle: Double) {
val x = toPixel(centerX)
val y = toPixel(centerY)
val from = startAngle / 180 * PI
val to = (startAngle + swipeAngle) / 180 * PI
ctx.beginPath()
ctx.moveTo(x, y)
ctx.arc(x, y, toPixel(radius), -from, -to, swipeAngle >= 0)
ctx.lineTo(x, y)
ctx.fill()
}
override fun fillCircle(centerX: Double, centerY: Double, radius: Double) {
ctx.beginPath()
ctx.arc(toPixel(centerX),
toPixel(centerY),
toPixel(radius),
0.0,
2 * PI)
ctx.fill()
}
override fun setTextAlign(align: TextAlign) {
this.align = when (align) {
TextAlign.LEFT -> CanvasTextAlign.LEFT
TextAlign.CENTER -> CanvasTextAlign.CENTER
TextAlign.RIGHT -> CanvasTextAlign.RIGHT
}
}
override fun toImage(): Image {
return JsImage(this,
ctx.getImageData(0.0,
0.0,
element.width.toDouble(),
element.height.toDouble()))
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.gui
import org.khronos.webgl.*
import org.w3c.dom.*
import kotlin.browser.*
import kotlin.math.*
class JsImage(val canvas: JsCanvas,
val imageData: ImageData) : Image {
override val width: Int
get() = imageData.width
override val height: Int
get() = imageData.height
val pixels = imageData.unsafeCast<Uint16Array>()
init {
console.log(width, height, imageData.data.length)
}
override suspend fun export(path: String) {
canvas.ctx.putImageData(imageData, 0.0, 0.0)
val container = document.createElement("div")
container.className = "export"
val title = document.createElement("div")
title.innerHTML = path
document.body?.appendChild(container)
container.appendChild(title)
container.appendChild(canvas.element)
}
override fun getPixel(x: Int, y: Int): Color {
val offset = 4 * (y * width + x)
return Color(imageData.data[offset + 0] / 255.0,
imageData.data[offset + 1] / 255.0,
imageData.data[offset + 2] / 255.0,
imageData.data[offset + 3] / 255.0)
}
override fun setPixel(x: Int, y: Int, color: Color) {
val offset = 4 * (y * width + x)
inline fun map(x: Double): Byte {
return (x * 255).roundToInt().unsafeCast<Byte>()
}
imageData.data.set(offset + 0, map(color.red))
imageData.data.set(offset + 1, map(color.green))
imageData.data.set(offset + 2, map(color.blue))
imageData.data.set(offset + 3, map(color.alpha))
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.io
external fun require(module: String): dynamic
class JsPreparedStatement(val stmt: dynamic) : PreparedStatement {
override fun step(): StepResult {
val isRowAvailable = stmt.step() as Boolean
return if(isRowAvailable) StepResult.ROW else StepResult.DONE
}
override fun finalize() {
stmt.free()
}
override fun getInt(index: Int): Int {
return (stmt.getNumber(index) as Double).toInt()
}
override fun getLong(index: Int): Long {
return (stmt.getNumber(index) as Double).toLong()
}
override fun getText(index: Int): String {
return stmt.getString(index) as String
}
override fun getReal(index: Int): Double {
return stmt.getNumber(index) as Double
}
override fun bindInt(index: Int, value: Int) {
stmt.bindNumber(value, index + 1)
}
override fun bindLong(index: Int, value: Long) {
stmt.bindNumber(value, index + 1)
}
override fun bindText(index: Int, value: String) {
stmt.bindString(value, index + 1)
}
override fun bindReal(index: Int, value: Double) {
stmt.bindNumber(value, index + 1)
}
override fun reset() {
stmt.reset()
}
}
class JsDatabase(val db: dynamic) : Database {
override fun prepareStatement(sql: String): PreparedStatement {
return JsPreparedStatement(db.prepare(sql))
}
override fun close() {
}
}

View File

@@ -0,0 +1,165 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.io
import kotlinx.coroutines.*
import org.isoron.platform.gui.*
import org.isoron.platform.gui.Image
import org.w3c.dom.*
import org.w3c.xhr.*
import kotlin.browser.*
import kotlin.js.*
class JsFileStorage {
private val TAG = "JsFileStorage"
private val log = StandardLog()
private val indexedDB = eval("indexedDB")
private var db: dynamic = null
private val DB_NAME = "Main"
private val OS_NAME = "Files"
suspend fun init() {
log.info(TAG, "Initializing")
Promise<Int> { resolve, reject ->
val req = indexedDB.open(DB_NAME, 2)
req.onerror = { reject(Exception("could not open IndexedDB")) }
req.onupgradeneeded = {
log.info(TAG, "Creating document store")
req.result.createObjectStore(OS_NAME)
}
req.onsuccess = {
log.info(TAG, "Ready")
db = req.result
resolve(0)
}
}.await()
}
suspend fun delete(path: String) {
Promise<Int> { resolve, reject ->
val transaction = db.transaction(OS_NAME, "readwrite")
val os = transaction.objectStore(OS_NAME)
val req = os.delete(path)
req.onerror = { reject(Exception("could not delete $path")) }
req.onsuccess = { resolve(0) }
}.await()
}
suspend fun put(path: String, content: String) {
Promise<Int> { resolve, reject ->
val transaction = db.transaction(OS_NAME, "readwrite")
val os = transaction.objectStore(OS_NAME)
val req = os.put(content, path)
req.onerror = { reject(Exception("could not put $path")) }
req.onsuccess = { resolve(0) }
}.await()
}
suspend fun get(path: String): String {
return Promise<String> { resolve, reject ->
val transaction = db.transaction(OS_NAME, "readonly")
val os = transaction.objectStore(OS_NAME)
val req = os.get(path)
req.onerror = { reject(Exception("could not get $path")) }
req.onsuccess = { resolve(req.result) }
}.await()
}
suspend fun exists(path: String): Boolean {
return Promise<Boolean> { resolve, reject ->
val transaction = db.transaction(OS_NAME, "readonly")
val os = transaction.objectStore(OS_NAME)
val req = os.count(path)
req.onerror = { reject(Exception("could not count $path")) }
req.onsuccess = { resolve(req.result > 0) }
}.await()
}
}
class JsFileOpener(val fileStorage: JsFileStorage) : FileOpener {
override fun openUserFile(path: String): UserFile {
return JsUserFile(fileStorage, path)
}
override fun openResourceFile(path: String): ResourceFile {
return JsResourceFile(path)
}
}
class JsUserFile(val fs: JsFileStorage,
val filename: String) : UserFile {
override suspend fun lines(): List<String> {
return fs.get(filename).lines()
}
override suspend fun delete() {
fs.delete(filename)
}
override suspend fun exists(): Boolean {
return fs.exists(filename)
}
}
class JsResourceFile(val filename: String) : ResourceFile {
override suspend fun exists(): Boolean {
return Promise<Boolean> { resolve, reject ->
val xhr = XMLHttpRequest()
xhr.open("GET", "/assets/$filename", true)
xhr.onload = { resolve(xhr.status.toInt() != 404) }
xhr.onerror = { reject(Exception()) }
xhr.send()
}.await()
}
override suspend fun lines(): List<String> {
return Promise<List<String>> { resolve, reject ->
val xhr = XMLHttpRequest()
xhr.open("GET", "/assets/$filename", true)
xhr.onload = { resolve(xhr.responseText.lines()) }
xhr.onerror = { reject(Exception()) }
xhr.send()
}.await()
}
override suspend fun copyTo(dest: UserFile) {
val fs = (dest as JsUserFile).fs
fs.put(dest.filename, lines().joinToString("\n"))
}
override suspend fun toImage(): Image {
return Promise<Image> { resolve, reject ->
val img = org.w3c.dom.Image()
img.onload = {
val canvas = JsCanvas(document.createElement("canvas") as HTMLCanvasElement, 1.0)
canvas.element.width = img.naturalWidth
canvas.element.height = img.naturalHeight
canvas.setColor(Color(0xffffff))
canvas.fillRect(0.0, 0.0, canvas.getWidth(), canvas.getHeight())
canvas.ctx.drawImage(img, 0.0, 0.0)
resolve(canvas.toImage())
}
img.src = "/assets/$filename"
}.await()
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.io
actual fun format(format: String, arg: String): String {
return js("vsprintf")(format, arg) as String
}
actual fun format(format: String, arg: Int): String {
return js("vsprintf")(format, arg) as String
}
actual fun format(format: String, arg: Double): String {
return js("vsprintf")(format, arg) as String
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.time
import kotlin.js.*
fun LocalDate.toJsDate(): Date {
return Date(year, month - 1, day)
}
class JsDateFormatter(private val locale: String) : LocalDateFormatter {
override fun shortWeekdayName(date: LocalDate): String {
val options = dateLocaleOptions { weekday = "short" }
return date.toJsDate().toLocaleString(locale, options)
}
override fun shortMonthName(date: LocalDate): String {
val options = dateLocaleOptions { month = "short" }
return date.toJsDate().toLocaleString(locale, options)
}
}

View File

@@ -0,0 +1,168 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.gui
import kotlinx.coroutines.*
import org.isoron.platform.io.*
import java.awt.*
import java.awt.RenderingHints.*
import java.awt.font.*
import java.awt.image.*
import kotlin.math.*
class JavaCanvas(val image: BufferedImage,
val pixelScale: Double = 2.0) : Canvas {
override fun toImage(): Image {
return JavaImage(image)
}
private val frc = FontRenderContext(null, true, true)
private var fontSize = 12.0
private var font = Font.REGULAR
private var textAlign = TextAlign.CENTER
val widthPx = image.width
val heightPx = image.height
val g2d = image.createGraphics()
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")
init {
g2d.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(KEY_TEXT_ANTIALIASING, VALUE_TEXT_ANTIALIAS_ON);
g2d.setRenderingHint(KEY_FRACTIONALMETRICS, VALUE_FRACTIONALMETRICS_ON);
updateFont()
}
private fun toPixel(x: Double): Int {
return (pixelScale * x).toInt()
}
private fun toDp(x: Int): Double {
return x / pixelScale
}
override fun setColor(color: Color) {
g2d.color = java.awt.Color(color.red.toFloat(),
color.green.toFloat(),
color.blue.toFloat(),
color.alpha.toFloat())
}
override fun drawLine(x1: Double, y1: Double, x2: Double, y2: Double) {
g2d.drawLine(toPixel(x1), toPixel(y1), toPixel(x2), toPixel(y2))
}
override fun drawText(text: String, x: Double, y: Double) {
updateFont()
val bounds = g2d.font.getStringBounds(text, frc)
val bWidth = bounds.width.roundToInt()
val bHeight = bounds.height.roundToInt()
val bx = bounds.x.roundToInt()
val by = bounds.y.roundToInt()
if (textAlign == TextAlign.CENTER) {
g2d.drawString(text,
toPixel(x) - bx - bWidth / 2,
toPixel(y) - by - bHeight / 2)
} else if (textAlign == TextAlign.LEFT) {
g2d.drawString(text,
toPixel(x) - bx,
toPixel(y) - by - bHeight / 2)
} else {
g2d.drawString(text,
toPixel(x) - bx - bWidth,
toPixel(y) - by - bHeight / 2)
}
}
override fun fillRect(x: Double, y: Double, width: Double, height: Double) {
g2d.fillRect(toPixel(x), toPixel(y), toPixel(width), toPixel(height))
}
override fun drawRect(x: Double, y: Double, width: Double, height: Double) {
g2d.drawRect(toPixel(x), toPixel(y), toPixel(width), toPixel(height))
}
override fun getHeight(): Double {
return toDp(heightPx)
}
override fun getWidth(): Double {
return toDp(widthPx)
}
override fun setFont(font: Font) {
this.font = font
updateFont()
}
override fun setFontSize(size: Double) {
fontSize = size
updateFont()
}
override fun setStrokeWidth(size: Double) {
g2d.stroke = BasicStroke((size * pixelScale).toFloat())
}
private fun updateFont() {
val size = (fontSize * pixelScale).toFloat()
g2d.font = when (font) {
Font.REGULAR -> NOTO_REGULAR_FONT.deriveFont(size)
Font.BOLD -> NOTO_BOLD_FONT.deriveFont(size)
Font.FONT_AWESOME -> FONT_AWESOME_FONT.deriveFont(size)
}
}
override fun fillCircle(centerX: Double, centerY: Double, radius: Double) {
g2d.fillOval(toPixel(centerX - radius),
toPixel(centerY - radius),
toPixel(radius * 2),
toPixel(radius * 2))
}
override fun fillArc(centerX: Double,
centerY: Double,
radius: Double,
startAngle: Double,
swipeAngle: Double) {
g2d.fillArc(toPixel(centerX - radius),
toPixel(centerY - radius),
toPixel(radius * 2),
toPixel(radius * 2),
startAngle.roundToInt(),
swipeAngle.roundToInt())
}
override fun setTextAlign(align: TextAlign) {
this.textAlign = align
}
private fun createFont(path: String) = runBlocking<java.awt.Font> {
val file = JavaFileOpener().openResourceFile(path) as JavaResourceFile
if (!file.exists()) throw RuntimeException("File not found: ${file.path}")
java.awt.Font.createFont(0, file.stream())
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.gui
import java.awt.image.*
import java.io.*
import javax.imageio.*
class JavaImage(val bufferedImage: BufferedImage) : Image {
override fun setPixel(x: Int, y: Int, color: Color) {
bufferedImage.setRGB(x, y, java.awt.Color(color.red.toFloat(),
color.green.toFloat(),
color.blue.toFloat()).rgb)
}
override suspend fun export(path: String) {
val file = File(path)
file.parentFile.mkdirs()
ImageIO.write(bufferedImage, "png", file)
}
override val width: Int
get() = bufferedImage.width
override val height: Int
get() = bufferedImage.height
override fun getPixel(x: Int, y: Int): Color {
return Color(bufferedImage.getRGB(x, y))
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.io
import java.sql.*
import java.sql.PreparedStatement
class JavaPreparedStatement(private var stmt: PreparedStatement) : org.isoron.platform.io.PreparedStatement {
private var rs: ResultSet? = null
private var hasExecuted = false
override fun step(): StepResult {
if (!hasExecuted) {
hasExecuted = true
val hasResult = stmt.execute()
if (hasResult) rs = stmt.resultSet
}
if (rs == null || !rs!!.next()) return StepResult.DONE
return StepResult.ROW
}
override fun finalize() {
stmt.close()
}
override fun getInt(index: Int): Int {
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)
}
override fun getReal(index: Int): Double {
return rs!!.getDouble(index + 1)
}
override fun bindInt(index: Int, value: Int) {
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)
}
override fun bindReal(index: Int, value: Double) {
stmt.setDouble(index + 1, value)
}
override fun reset() {
stmt.clearParameters()
hasExecuted = false
}
}
class JavaDatabase(private var conn: Connection,
private val log: Log) : Database {
override fun prepareStatement(sql: String): org.isoron.platform.io.PreparedStatement {
return JavaPreparedStatement(conn.prepareStatement(sql))
}
override fun close() {
conn.close()
}
}
class JavaDatabaseOpener(val log: Log) : DatabaseOpener {
override fun open(file: UserFile): Database {
val platformFile = file as JavaUserFile
val conn = DriverManager.getConnection("jdbc:sqlite:${platformFile.path}")
return JavaDatabase(conn, log)
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.io
import org.isoron.platform.gui.*
import java.io.*
import java.nio.file.*
import javax.imageio.*
class JavaResourceFile(val path: String) : ResourceFile {
private val javaPath: Path
get() {
val mainPath = Paths.get("assets/main/$path")
val testPath = Paths.get("assets/test/$path")
if (Files.exists(mainPath)) return mainPath
else return testPath
}
override suspend fun exists(): Boolean {
return Files.exists(javaPath)
}
override suspend fun lines(): List<String> {
return Files.readAllLines(javaPath)
}
override suspend fun copyTo(dest: UserFile) {
if (dest.exists()) dest.delete()
val destPath = (dest as JavaUserFile).path
destPath.toFile().parentFile?.mkdirs()
Files.copy(javaPath, destPath)
}
fun stream(): InputStream {
return Files.newInputStream(javaPath)
}
override suspend fun toImage(): Image {
return JavaImage(ImageIO.read(stream()))
}
}
class JavaUserFile(val path: Path) : UserFile {
override suspend fun lines(): List<String> {
return Files.readAllLines(path)
}
override suspend fun exists(): Boolean {
return Files.exists(path)
}
override suspend fun delete() {
Files.delete(path)
}
}
class JavaFileOpener : FileOpener {
override fun openUserFile(path: String): UserFile {
val path = Paths.get("/tmp/$path")
return JavaUserFile(path)
}
override fun openResourceFile(path: String): ResourceFile {
return JavaResourceFile(path)
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.io
actual fun format(format: String, arg: String): String =
String.format(format, arg)
actual fun format(format: String, arg: Int): String =
String.format(format, arg)
actual fun format(format: String, arg: Double): String =
String.format(format, arg)

View File

@@ -0,0 +1,52 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.time
import java.util.*
import java.util.Calendar.*
fun LocalDate.toGregorianCalendar(): GregorianCalendar {
val cal = GregorianCalendar()
cal.timeZone = TimeZone.getTimeZone("GMT")
cal.set(MILLISECOND, 0)
cal.set(SECOND, 0)
cal.set(MINUTE, 0)
cal.set(HOUR_OF_DAY, 0)
cal.set(YEAR, this.year)
cal.set(MONTH, this.month - 1)
cal.set(DAY_OF_MONTH, this.day)
return cal
}
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);
}
}