Remove unused project dirs (uhabits-core-legacy, uhabits-ios, etc)

pull/2128/head
Alinson S. Xavier 6 months ago
parent 107c898f51
commit 3330014fa9

@ -12,17 +12,6 @@
Please see `docs/BUILD.md` and `docs/TEST.md`
## Directory Layout
* `docs` Documentation for developers.
* `landing` Source code for our [landing page](http://loophabits.org/).
* `uhabits-android` Android-specific code.
* `uhabits-core` Common code used by all platforms (Android, iOS).
* `uhabits-core-legacy` Proof-of-concept module, developed to evaluate the feasibility of using Kotlin multiplatform for the app; not currently used, and it will be removed soon, once all useful code is ported to the other modules.
* `uhabits-ios` Experimental iOS port of Loop. Not currently used in production.
* `uhabits-server` Source code for any server-side components the app (for example, device sync).
* `uhabits-web` Experimental web port of Loop. Not currently used in production.
## Branching Policy
This repository uses the [git-flow branching model](https://nvie.com/posts/a-successful-git-branching-model/). Basically, there are two main branches, `dev` and `master`. All the development takes place in the `dev` branch. After the new features have been implemented and tested, they are merged into the `master` branch and a new version of the app is released. Please submit your pull requests against the `dev` branch.

@ -1,30 +0,0 @@
#!/bin/bash
input=$1
locale_name=$2
cat <<END
// --------------------------------------------------------------------------
// THIS FILE WAS AUTOMATICALLY GENERATED
//
// Please do not submit pull request to modify it. Corrections to translations
// may be submitted at https://translate.loophabits.org/
// --------------------------------------------------------------------------
package org.isoron.uhabits.i18n
END
prefix="override "
if [ "$locale_name" == "" ]; then
prefix="open "
echo "open class Strings() {"
else
echo "class Strings$locale_name : Strings() {"
fi
grep "<string name" "$1" | \
grep -v translatable | \
sed 's/&amp;/\&/g' | \
sed 's/^.*name="\([^"]*\)">\([^<]*\)<.*/ '"$prefix"'val \1 = "\2"/'
echo "}"

@ -1,46 +0,0 @@
#!/bin/bash
INPUT_DIR=../android/uhabits-android/src/main/res/
OUTPUT_DIR=../core/src/commonMain/kotlin/org/isoron/uhabits/i18n/
convert() {
./androidStringsToKt.sh $INPUT_DIR/$1/strings.xml "$2" > $OUTPUT_DIR/Strings$2.kt
}
#convert values ""
convert values-ar Arabic
convert values-bg Bulgarian
convert values-ca Catalan
convert values-cs Czech
convert values-da Danish
convert values-de German
convert values-el Greek
convert values-eo Esperanto
convert values-es Spanish
convert values-eu Basque
convert values-fa Persian
convert values-fi Finnish
convert values-fr French
convert values-hi Hindi
convert values-hr Croatian
convert values-hu Hungarian
convert values-in Indonesian
convert values-it Italian
convert values-iw Hebrew
convert values-ja Japanese
convert values-ko Korean
convert values-nl Dutch
convert values-no-rNO Norwegian
convert values-pl Polish
convert values-pt-rBR PortugueseBR
convert values-pt-rPT PortuguesePT
convert values-ro Romanian
convert values-ru Russian
convert values-sl Slovak
convert values-sr Serbian
convert values-sv Swedish
convert values-tr Turkish
convert values-uk Ukrainian
convert values-vi Vietnamese
convert values-zh-rCN ChineseCN
convert values-zh-rTW ChineseTW

@ -1,5 +0,0 @@
create table Habits ( id integer primary key autoincrement, archived integer, color integer, description text, freq_den integer, freq_num integer, highlight integer, name text, position integer, reminder_hour integer, reminder_min integer )
create table Checkmarks ( id integer primary key autoincrement, habit integer references habits(id), timestamp integer, value integer )
create table Repetitions ( id integer primary key autoincrement, habit integer references habits(id), timestamp integer )
create table Streak ( id integer primary key autoincrement, end integer, habit integer references habits(id), length integer, start integer )
create table Score ( id integer primary key autoincrement, habit integer references habits(id), score integer, timestamp integer )

@ -1,3 +0,0 @@
delete from Score
delete from Streak
delete from Checkmarks

@ -1 +0,0 @@
alter table Habits add column reminder_days integer not null default 127

@ -1,3 +0,0 @@
delete from Score
delete from Streak
delete from Checkmarks

@ -1,4 +0,0 @@
create index idx_score_habit_timestamp on Score(habit, timestamp)
create index idx_checkmark_habit_timestamp on Checkmarks(habit, timestamp)
create index idx_repetitions_habit_timestamp on Repetitions(habit, timestamp)
create index idx_streak_habit_end on Streak(habit, end)

@ -1,14 +0,0 @@
update habits set color=0 where color=-2937041
update habits set color=1 where color=-1684967
update habits set color=2 where color=-415707
update habits set color=3 where color=-5262293
update habits set color=4 where color=-13070788
update habits set color=5 where color=-16742021
update habits set color=6 where color=-16732991
update habits set color=7 where color=-16540699
update habits set color=8 where color=-10603087
update habits set color=9 where color=-7461718
update habits set color=10 where color=-2614432
update habits set color=11 where color=-13619152
update habits set color=12 where color=-5592406
update habits set color=0 where color<0 or color>12

@ -1,3 +0,0 @@
delete from Score
delete from Streak
delete from Checkmarks

@ -1,2 +0,0 @@
alter table Habits add column type integer not null default 0
alter table Repetitions add column value integer not null default 2

@ -1,5 +0,0 @@
drop table Score
create table Score ( id integer primary key autoincrement, habit integer references habits(id), score real, timestamp integer)
create index idx_score_habit_timestamp on Score(habit, timestamp)
delete from streak
delete from checkmarks

@ -1,3 +0,0 @@
alter table Habits add column target_type integer not null default 0
alter table Habits add column target_value real not null default 0
alter table Habits add column unit text not null default ""

@ -1 +0,0 @@
create table Events ( id integer primary key autoincrement, timestamp integer, message text, server_id integer )

@ -1,3 +0,0 @@
drop table checkmarks
drop table streak
drop table score

@ -1,12 +0,0 @@
update habits set color=19 where color=12
update habits set color=17 where color=11
update habits set color=15 where color=10
update habits set color=14 where color=9
update habits set color=13 where color=8
update habits set color=10 where color=7
update habits set color=9 where color=6
update habits set color=8 where color=5
update habits set color=7 where color=4
update habits set color=5 where color=3
update habits set color=4 where color=2
update habits set color=0 where color<0 or color>19

@ -1,11 +0,0 @@
delete from repetitions where habit not in (select id from habits)
delete from repetitions where timestamp is null
delete from repetitions where habit is null
delete from repetitions where rowid not in ( select min(rowid) from repetitions group by habit, timestamp )
alter table Repetitions rename to RepetitionsBak
create table Repetitions ( id integer primary key autoincrement, habit integer not null references habits(id), timestamp integer not null, value integer not null)
drop index if exists idx_repetitions_habit_timestamp
create unique index idx_repetitions_habit_timestamp on Repetitions( habit, timestamp)
insert into Repetitions select * from RepetitionsBak
drop table RepetitionsBak
pragma foreign_keys=ON

@ -1 +0,0 @@
create table Preferences(key text unique not null, value text not null)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

@ -1,2 +0,0 @@
Hello World!
This is a resource.

@ -1,136 +0,0 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
plugins {
id 'kotlin-multiplatform' version "1.3.72"
}
repositories {
mavenCentral()
}
kotlin {
targets {
def sdkName = System.getenv('SDK_NAME')
def isIphone = sdkName?.startsWith("iphoneos")
def iosPreset = isIphone ? presets.iosArm64 : presets.iosX64
fromPreset(iosPreset, 'ios') {
binaries {
framework {
baseName = "LoopHabitTracker"
}
}
compilations.main {
cinterops {
sqlite3 {
defFile project.file("src/main/c_interop/sqlite3.def")
}
}
}
}
fromPreset(presets.jvm, 'jvm') {
}
fromPreset(presets.js, 'js') {
compilations.main.kotlinOptions {
moduleKind = "commonjs"
}
compilations.test.kotlinOptions {
moduleKind = "commonjs"
}
}
}
sourceSets {
commonMain {
kotlin { srcDir "src/main/common" }
dependencies {
implementation kotlin('stdlib-common')
implementation "$KX_COROUTINES-core-common:$KX_COROUTINES_VERSION"
}
}
commonTest {
kotlin { srcDir "src/test/common" }
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
}
}
jvmMain {
kotlin { srcDir "src/main/jvm" }
dependencies {
implementation kotlin('stdlib-jdk8')
implementation "$KX_COROUTINES-core:$KX_COROUTINES_VERSION"
}
}
jvmTest {
kotlin { srcDir "src/test/jvm" }
dependencies {
implementation kotlin('test')
implementation kotlin('test-junit')
implementation 'org.xerial:sqlite-jdbc:3.25.2'
}
}
jsMain {
kotlin { srcDir "src/main/js" }
dependencies {
implementation kotlin('stdlib-js')
implementation "$KX_COROUTINES-core-js:$KX_COROUTINES_VERSION"
}
}
jsTest {
kotlin { srcDir "src/test/js" }
dependencies {
implementation kotlin('test-js')
}
}
iosMain {
kotlin { srcDir "src/main/ios" }
dependencies {
implementation "$KX_COROUTINES-core-native:$KX_COROUTINES_VERSION"
}
}
iosTest {
kotlin { srcDir "src/test/ios" }
dependencies {
implementation "$KX_COROUTINES-core-native:$KX_COROUTINES_VERSION"
}
}
}
task iosTestCopyResources(type: Copy) {
from 'assets/test/'
from 'assets/main/'
into 'build/bin/ios/debugTest'
}
if (project.tasks.findByName('iosTest')) {
iosTest.dependsOn(iosTestCopyResources)
}
}

@ -1,2 +0,0 @@
KX_COROUTINES_VERSION=1.3.6
KX_COROUTINES=org.jetbrains.kotlinx:kotlinx-coroutines

@ -1,9 +0,0 @@
pluginManagement {
resolutionStrategy {
eachPlugin {
if (requested.id.id == "kotlin-multiplatform") {
useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:${requested.version}")
}
}
}
}

@ -1,25 +0,0 @@
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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,190 +0,0 @@
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"
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save