diff --git a/core/assets/main/databases/template.db b/core/assets/main/databases/template.db new file mode 100644 index 000000000..05c67f527 Binary files /dev/null and b/core/assets/main/databases/template.db differ diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/Backend.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/Backend.kt index 13defaee5..e58145d67 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/Backend.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/Backend.kt @@ -19,26 +19,27 @@ package org.isoron.uhabits -import org.isoron.uhabits.models.HabitList +import org.isoron.uhabits.models.* import org.isoron.uhabits.utils.* class Backend(var databaseOpener: DatabaseOpener, var fileOpener: FileOpener, var log: Log) { - var db: Database - - var habits: HabitList + var database: Database + var habitsRepository: HabitRepository + var habits = mutableMapOf() init { - val dbFile = fileOpener.openUserFile("uhabits.sqlite3") - db = databaseOpener.open(dbFile) - db.migrateTo(LOOP_DATABASE_VERSION, fileOpener, log) - habits = HabitList(db) + val dbFile = fileOpener.openUserFile("uhabits.db") + database = databaseOpener.open(dbFile) + database.migrateTo(LOOP_DATABASE_VERSION, fileOpener, log) + habitsRepository = HabitRepository(database) + habits = habitsRepository.findAll() } fun getHabitList(): List> { - return habits.getActive().map { h -> + return habits.values.sortedBy { h -> h.position }.map { h -> mapOf("key" to h.id.toString(), "name" to h.name, "color" to h.color.paletteIndex) @@ -46,11 +47,30 @@ class Backend(var databaseOpener: DatabaseOpener, } fun createHabit(name: String) { + val id = habitsRepository.nextId() + val habit = Habit(id = id, + name = name, + description = "", + frequency = Frequency(1, 1), + color = Color(3), + isArchived = false, + position = habits.size, + unit = "", + target = 0.0, + type = HabitType.YES_NO_HABIT) + habitsRepository.insert(habit) + habits[id] = habit } fun deleteHabit(id: Int) { + val habit = habits[id]!! + habitsRepository.delete(habit) + habits.remove(id) } fun updateHabit(id: Int, name: String) { + val habit = habits[id]!! + habit.name = name + habitsRepository.update(habit) } } diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/models/Color.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/models/Color.kt index e34ddc82e..d9ae08eae 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/models/Color.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/models/Color.kt @@ -19,4 +19,4 @@ package org.isoron.uhabits.models -class Color(val paletteIndex: Int) \ No newline at end of file +data class Color(val paletteIndex: Int) \ No newline at end of file diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/models/Habit.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/models/Habit.kt index 2c4fe75b8..8f4897c82 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/models/Habit.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/models/Habit.kt @@ -27,5 +27,5 @@ data class Habit(var id: Int, var isArchived: Boolean, var position: Int, var unit: String, - var target: Int, + var target: Double, var type: HabitType) \ No newline at end of file diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/models/HabitList.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/models/HabitList.kt deleted file mode 100644 index 427b1b452..000000000 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/models/HabitList.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2016-2019 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.models - -import org.isoron.uhabits.utils.Database -import org.isoron.uhabits.utils.StepResult - -class HabitList(var db: Database) { - var habits = mutableListOf() - - init { - loadHabitsFromDatabase() - } - - fun getActive(): List { - return habits.filter { h -> !h.isArchived } - } - - private fun loadHabitsFromDatabase() { - val stmt = db.prepareStatement( - "select id, name, description, freq_num, freq_den, color, " + - "archived, position, unit, target_value, type " + - "from habits") - - while (stmt.step() == StepResult.ROW) { - habits.add(Habit(id = stmt.getInt(0), - name = stmt.getText(1), - description = stmt.getText(2), - frequency = Frequency(stmt.getInt(3), - stmt.getInt(4)), - color = Color(stmt.getInt(5)), - isArchived = (stmt.getInt(6) != 0), - position = stmt.getInt(7), - unit = stmt.getText(8), - target = 0, - type = HabitType.YES_NO_HABIT)) - } - stmt.finalize() - } -} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/models/HabitRepository.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/models/HabitRepository.kt new file mode 100644 index 000000000..188cd81ab --- /dev/null +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/models/HabitRepository.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.models + +import org.isoron.uhabits.utils.Database +import org.isoron.uhabits.utils.PreparedStatement +import org.isoron.uhabits.utils.StepResult +import org.isoron.uhabits.utils.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 { + val result = mutableMapOf() + 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 = Color(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.YES_NO_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.paletteIndex) + 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() + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/models/HabitType.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/models/HabitType.kt index 8965f5c09..3fb8ba57f 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/models/HabitType.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/models/HabitType.kt @@ -19,7 +19,7 @@ package org.isoron.uhabits.models -enum class HabitType { - YES_NO_HABIT, - NUMERICAL_HABIT, +enum class HabitType(val code: Int) { + YES_NO_HABIT(0), + NUMERICAL_HABIT(1), } \ No newline at end of file diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Database.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Database.kt index e4f827aca..98dfdbb5c 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Database.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Database.kt @@ -29,8 +29,10 @@ interface PreparedStatement { fun finalize() fun getInt(index: Int): Int fun getText(index: Int): String + fun getReal(index: Int): Double fun bindInt(index: Int, value: Int) fun bindText(index: Int, value: String) + fun bindReal(index: Int, value: Double) fun reset() } @@ -57,25 +59,38 @@ fun Database.queryInt(sql: String): Int { 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() = execute("begin") + fun Database.commit() = execute("commit") -fun Database.rollback() = execute("rollback") + fun Database.getVersion() = queryInt("pragma user_version") + fun Database.setVersion(v: Int) = execute("pragma user_version = $v") fun Database.migrateTo(newVersion: Int, fileOpener: FileOpener, log: Log) { val currentVersion = getVersion() - log.debug("Current database version: $currentVersion") + log.debug("Database", "Current database version: $currentVersion") if (currentVersion == newVersion) return - log.debug("Upgrading to version: $newVersion") + 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) { - log.debug("Running migration $v") + log.debug("Database", "Running migration $v") val filename = sprintf("migrations/%03d.sql", v) val migrationFile = fileOpener.openResourceFile(filename) for (line in migrationFile.readLines()) { diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Log.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Log.kt index d359b3911..be5f0d3fc 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Log.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Log.kt @@ -20,6 +20,20 @@ package org.isoron.uhabits.utils interface Log { - fun info(msg: String) - fun debug(msg: String) + fun info(tag: String, msg: String) + fun debug(tag: String, msg: String) +} + +/** + * A Log that prints to the standard output. + */ +class StandardLog : Log { + override fun info(tag: String, msg: String) { + println(sprintf("I/%-20s %s", tag, msg)) + } + + override fun debug(tag: String, msg: String) { + println(sprintf("D/%-20s %s", tag, msg)) + } + } \ No newline at end of file diff --git a/core/src/iosMain/kotlin/org/isoron/uhabits/utils/IosFiles.kt b/core/src/iosMain/kotlin/org/isoron/uhabits/utils/IosFiles.kt new file mode 100644 index 000000000..86e61fc86 --- /dev/null +++ b/core/src/iosMain/kotlin/org/isoron/uhabits/utils/IosFiles.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.utils + +import platform.Foundation.* + +class IosResourceFile(val path: String) : ResourceFile { + private val fileManager = NSFileManager.defaultManager() + override fun readLines(): List { + val contents = NSString.stringWithContentsOfFile(path) as NSString + return contents.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet()) as List + } +} + +class IosUserFile(val path: String) : UserFile { + override fun exists(): Boolean { + return NSFileManager.defaultManager().fileExistsAtPath(path) + } + + override fun delete() { + NSFileManager.defaultManager().removeItemAtPath(path, null) + } +} + +class IosFileOpener : FileOpener { + override fun openResourceFile(filename: String): ResourceFile { + val root = NSBundle.mainBundle.resourcePath!! + return IosResourceFile("$root/$filename") + } + + override fun openUserFile(filename: String): UserFile { + val manager = NSFileManager.defaultManager() + val root = manager.URLForDirectory(NSDocumentDirectory, + NSUserDomainMask, + null, false, null)!!.path + return IosUserFile("$root/$filename") + } +} \ No newline at end of file diff --git a/core/src/jvmMain/kotlin/org/isoron/uhabits/database/JavaDatabase.kt b/core/src/jvmMain/kotlin/org/isoron/uhabits/utils/JavaDatabase.kt similarity index 82% rename from core/src/jvmMain/kotlin/org/isoron/uhabits/database/JavaDatabase.kt rename to core/src/jvmMain/kotlin/org/isoron/uhabits/utils/JavaDatabase.kt index 2b4004c0f..a0a52ce5f 100644 --- a/core/src/jvmMain/kotlin/org/isoron/uhabits/database/JavaDatabase.kt +++ b/core/src/jvmMain/kotlin/org/isoron/uhabits/utils/JavaDatabase.kt @@ -17,17 +17,16 @@ * with this program. If not, see . */ -package org.isoron.uhabits.database +package org.isoron.uhabits.utils -import org.isoron.uhabits.utils.* import java.sql.Connection import java.sql.DriverManager import java.sql.PreparedStatement import java.sql.ResultSet -class JavaPreparedStatement(private var stmt : PreparedStatement) : org.isoron.uhabits.utils.PreparedStatement { - +class JavaPreparedStatement(private var stmt: PreparedStatement) : org.isoron.uhabits.utils.PreparedStatement { private var rs: ResultSet? = null + private var hasExecuted = false override fun step(): StepResult { @@ -40,7 +39,6 @@ class JavaPreparedStatement(private var stmt : PreparedStatement) : org.isoron.u if (rs == null || !rs!!.next()) return StepResult.DONE return StepResult.ROW } - override fun finalize() { stmt.close() } @@ -53,16 +51,25 @@ class JavaPreparedStatement(private var stmt : PreparedStatement) : org.isoron.u return rs!!.getString(index + 1) } + override fun getReal(index: Int): Double { + return rs!!.getDouble(index + 1) + } + override fun bindInt(index: Int, value: Int) { - stmt.setInt(index, value) + stmt.setInt(index + 1, value) } override fun bindText(index: Int, value: String) { - stmt.setString(index, value) + stmt.setString(index + 1, value) + } + + override fun bindReal(index: Int, value: Double) { + stmt.setDouble(index + 1, value) } override fun reset() { stmt.clearParameters() + hasExecuted = false } } @@ -70,7 +77,7 @@ class JavaDatabase(private var conn: Connection, private val log: Log) : Database { override fun prepareStatement(sql: String): org.isoron.uhabits.utils.PreparedStatement { - log.debug("Running SQL: ${sql}") + log.debug("Database", "Preparing: $sql") return JavaPreparedStatement(conn.prepareStatement(sql)) } override fun close() { diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/BaseTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/BaseTest.kt index 2b6ec7ded..26e2f4689 100644 --- a/core/src/jvmTest/kotlin/org/isoron/uhabits/BaseTest.kt +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/BaseTest.kt @@ -19,11 +19,21 @@ package org.isoron.uhabits -import org.isoron.uhabits.utils.JavaFileOpener -import org.isoron.uhabits.database.JavaDatabaseOpener -import org.isoron.uhabits.utils.JavaLog +import org.isoron.uhabits.models.HabitRepository +import org.isoron.uhabits.utils.* +import org.junit.Before open class BaseTest { val fileOpener = JavaFileOpener() - val databaseOpener = JavaDatabaseOpener(JavaLog()) + val log = StandardLog() + val databaseOpener = JavaDatabaseOpener(log) + lateinit var db: Database + + @Before + open fun setUp() { + val dbFile = fileOpener.openUserFile("test.sqlite3") + if (dbFile.exists()) dbFile.delete() + db = databaseOpener.open(dbFile) + db.migrateTo(LOOP_DATABASE_VERSION, fileOpener, log) + } } \ No newline at end of file diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/database/JavaDatabaseTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/database/JavaDatabaseTest.kt index 4746c41a2..3400b95e1 100644 --- a/core/src/jvmTest/kotlin/org/isoron/uhabits/database/JavaDatabaseTest.kt +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/database/JavaDatabaseTest.kt @@ -26,15 +26,6 @@ import org.junit.Test import kotlin.test.assertEquals class JavaDatabaseTest : BaseTest() { - private lateinit var db: Database - - @Before - fun setup() { - val dbFile = fileOpener.openUserFile("test.sqlite3") - if (dbFile.exists()) dbFile.delete() - db = databaseOpener.open(dbFile) - } - @Test fun testUsage() { db.setVersion(0) @@ -51,14 +42,14 @@ class JavaDatabaseTest : BaseTest() { stmt.step() stmt.finalize() - stmt = db.prepareStatement("insert into demo(key, value) values (?1, ?2)") - stmt.bindInt(1, 42) - stmt.bindText(2, "Hello World") + stmt = db.prepareStatement("insert into demo(key, value) values (?, ?)") + stmt.bindInt(0, 42) + stmt.bindText(1, "Hello World") stmt.step() stmt.finalize() - stmt = db.prepareStatement("select * from demo where key > ?1") - stmt.bindInt(1, 10) + stmt = db.prepareStatement("select * from demo where key > ?") + stmt.bindInt(0, 10) var result = stmt.step() assertEquals(result, StepResult.ROW) @@ -71,12 +62,4 @@ class JavaDatabaseTest : BaseTest() { stmt.finalize() db.close() } - - @Test - fun testMigrateTo() { - assertEquals(0, db.getVersion()) - db.migrateTo(22, fileOpener) - assertEquals(22, db.getVersion()) - db.execute("select * from habits") - } } \ No newline at end of file diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/models/HabitRepositoryTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/models/HabitRepositoryTest.kt new file mode 100644 index 000000000..8897a9124 --- /dev/null +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/models/HabitRepositoryTest.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.models + +import junit.framework.Assert.assertEquals +import org.isoron.uhabits.BaseTest +import org.junit.Before +import org.junit.Test + +class HabitRepositoryTest : BaseTest() { + lateinit var repository: HabitRepository + + lateinit private var original0: Habit + lateinit private var original1: Habit + lateinit private var original2: Habit + + @Before + override fun setUp() { + super.setUp() + + original0 = Habit(id = 0, + name = "Wake up early", + description = "Did you wake up before 6am?", + frequency = Frequency(1, 1), + color = Color(3), + isArchived = false, + position = 0, + unit = "", + target = 0.0, + type = HabitType.YES_NO_HABIT) + + original1 = Habit(id = 1, + name = "Exercise", + description = "Did you exercise for at least 20 minutes?", + frequency = Frequency(1, 2), + color = Color(4), + isArchived = false, + position = 1, + unit = "", + target = 0.0, + type = HabitType.YES_NO_HABIT) + + original2 = Habit(id = 2, + name = "Learn Japanese", + description = "Did you study Japanese today?", + frequency = Frequency(1, 1), + color = Color(3), + isArchived = false, + position = 2, + unit = "", + target = 0.0, + type = HabitType.YES_NO_HABIT) + + repository = HabitRepository(db) + } + + @Test + fun testFindActive() { + var habits = repository.findAll() + assertEquals(0, repository.nextId()) + assertEquals(0, habits.size) + + repository.insert(original0) + repository.insert(original1) + repository.insert(original2) + habits = repository.findAll() + assertEquals(3, habits.size) + assertEquals(original0, habits[0]) + assertEquals(original1, habits[1]) + assertEquals(original2, habits[2]) + + assertEquals(3, repository.nextId()) + + original0.description = "New description" + repository.update(original0) + habits = repository.findAll() + assertEquals(original0, habits[0]) + + repository.delete(original0) + habits = repository.findAll() + assertEquals(2, habits.size) + assertEquals(original1, habits[1]) + assertEquals(original2, habits[2]) + } +} \ No newline at end of file diff --git a/core/src/jvmMain/kotlin/org/isoron/uhabits/utils/JavaLog.kt b/ios-native/Application/AppDelegate.swift similarity index 55% rename from core/src/jvmMain/kotlin/org/isoron/uhabits/utils/JavaLog.kt rename to ios-native/Application/AppDelegate.swift index 32d5ca248..d97c68a95 100644 --- a/core/src/jvmMain/kotlin/org/isoron/uhabits/utils/JavaLog.kt +++ b/ios-native/Application/AppDelegate.swift @@ -17,14 +17,23 @@ * with this program. If not, see . */ -package org.isoron.uhabits.utils +import UIKit -class JavaLog : Log { - override fun info(msg: String) { - println("[I] $msg") - } +@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - override fun debug(msg: String) { - println("[D] $msg") + window = UIWindow(frame: UIScreen.main.bounds) + if let window = window { + let nav = UINavigationController() + nav.viewControllers = [ListHabitsController()] + window.backgroundColor = UIColor.white + window.rootViewController = nav + window.makeKeyAndVisible() + } + return true } -} \ No newline at end of file +} diff --git a/ios-native/Application/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios-native/Application/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..d8db8d65f --- /dev/null +++ b/ios-native/Application/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ios-native/Application/Assets.xcassets/Contents.json b/ios-native/Application/Assets.xcassets/Contents.json new file mode 100644 index 000000000..da4a164c9 --- /dev/null +++ b/ios-native/Application/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ios-native/Application/EditHabit/EditHabitController.swift b/ios-native/Application/EditHabit/EditHabitController.swift new file mode 100644 index 000000000..5f7d5fc4e --- /dev/null +++ b/ios-native/Application/EditHabit/EditHabitController.swift @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +import UIKit + +class EditHabitTableViewController: NSObject, UITableViewDataSource, UITableViewDelegate { + func disclosure(title: String, subtitle: String) -> UITableViewCell { + let cell = UITableViewCell(style: .value1, reuseIdentifier: nil) + cell.textLabel?.text = title + cell.detailTextLabel?.text = subtitle + cell.accessoryType = .disclosureIndicator + return cell + } + + func input(title: String) -> UITableViewCell { + let cell = UITableViewCell() + let field = UITextField(frame: cell.bounds.insetBy(dx: 20, dy: 0)) + field.placeholder = title + field.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, + UIView.AutoresizingMask.flexibleHeight] + cell.contentView.addSubview(field) + return cell + } + + var primary = [UITableViewCell]() + var secondary = [UITableViewCell]() + var parentController: EditHabitController + + init(withParentController parentController: EditHabitController) { + self.parentController = parentController + super.init() + primary.append(input(title: "Name")) + primary.append(input(title: "Question (e.g. Did you wake up early today?)")) + secondary.append(disclosure(title: "Color", subtitle: "Blue")) + secondary.append(disclosure(title: "Repeat", subtitle: "Daily")) + secondary.append(disclosure(title: "Reminder", subtitle: "Disabled")) + } + + func numberOfSections(in tableView: UITableView) -> Int { + return 2 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return section == 0 ? primary.count : secondary.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + return indexPath.section == 0 ? primary[indexPath.item] : secondary[indexPath.item] + } + +// func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { +// let alert = UIAlertController(title: "Hello", message: "You selected something", preferredStyle: .alert) +// parentController.present(alert, animated: true) +// } +} + +class EditHabitController: UIViewController { + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + let bounds = UIScreen.main.bounds + let tableController = EditHabitTableViewController(withParentController: self) + let table = UITableView(frame: bounds, style: .grouped) + table.dataSource = tableController + table.delegate = tableController + self.view = table + } + + override func viewDidLoad() { + self.title = "Edit Habit" + } +} diff --git a/ios-native/Application/Info.plist b/ios-native/Application/Info.plist new file mode 100644 index 000000000..f1653a7f2 --- /dev/null +++ b/ios-native/Application/Info.plist @@ -0,0 +1,43 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UILaunchStoryboardName + Launch.storyboard + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/ios-native/Application/Launch.storyboard b/ios-native/Application/Launch.storyboard new file mode 100644 index 000000000..f9a048edf --- /dev/null +++ b/ios-native/Application/Launch.storyboard @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ios-native/Application/ListHabits/ListHabitsController.swift b/ios-native/Application/ListHabits/ListHabitsController.swift new file mode 100644 index 000000000..39c43ec80 --- /dev/null +++ b/ios-native/Application/ListHabits/ListHabitsController.swift @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +import UIKit + +class ListHabitsController: UIViewController { + + override func viewDidLoad() { + self.title = "Habits" + self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, + target: self, + action: #selector(self.onCreateHabitClicked)) + +// let box = UIView(frame: view.bounds.insetBy(dx: 100, dy: 100)) +// box.backgroundColor = .blue +//// box.translatesAutoresizingMaskIntoConstraints = true +// box.autoresizingMask = [UIView.AutoresizingMask.flexibleLeftMargin, +// UIView.AutoresizingMask.flexibleRightMargin] +// view.addSubview(box) + } + + @objc func onCreateHabitClicked() { + self.navigationController?.pushViewController(EditHabitController(), animated: true) + } +} diff --git a/ios-native/Tests/Info.plist b/ios-native/Tests/Info.plist new file mode 100644 index 000000000..6c40a6cd0 --- /dev/null +++ b/ios-native/Tests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/ios/uhabits.xcodeproj/project.pbxproj b/ios/uhabits.xcodeproj/project.pbxproj index 79b22d8d6..a74c6dbdb 100644 --- a/ios/uhabits.xcodeproj/project.pbxproj +++ b/ios/uhabits.xcodeproj/project.pbxproj @@ -18,8 +18,8 @@ 0021019C21F8AA3E00F9283D /* IosDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0021019B21F8AA3E00F9283D /* IosDatabase.swift */; }; 002101A421F936A300F9283D /* IosSqlDatabaseTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 002101A321F936A300F9283D /* IosSqlDatabaseTest.swift */; }; 002101AC21F9428C00F9283D /* core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0008A5C021F16D25000DB3E7 /* core.framework */; }; - 0091878521FD70B5001BDE6B /* IosLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0091878421FD70B5001BDE6B /* IosLog.swift */; }; - 00B2AC3D21FCA9D900CBEC8E /* IosFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00B2AC3C21FCA9D900CBEC8E /* IosFiles.swift */; }; + 00513C3B2200843F00702112 /* libthird-party.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DF0FFE32056DD460020B375 /* libthird-party.a */; }; + 00513C3C2200843F00702112 /* libyoga.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DAD3EA51DF850E9000B6D8A /* libyoga.a */; }; 00B2AC6521FD1A4500CBEC8E /* migrations in Resources */ = {isa = PBXBuildFile; fileRef = 00B2AC6421FD1A4500CBEC8E /* migrations */; }; 00B2AC6821FD1DA700CBEC8E /* IosFilesTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00B2AC6621FD1CEF00CBEC8E /* IosFilesTest.swift */; }; 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; }; @@ -339,8 +339,6 @@ 002101A121F936A300F9283D /* uhabitsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = uhabitsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 002101A321F936A300F9283D /* IosSqlDatabaseTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosSqlDatabaseTest.swift; sourceTree = ""; }; 002101A521F936A300F9283D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 0091878421FD70B5001BDE6B /* IosLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = IosLog.swift; path = uhabits/IosLog.swift; sourceTree = ""; }; - 00B2AC3C21FCA9D900CBEC8E /* IosFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = IosFiles.swift; path = uhabits/IosFiles.swift; sourceTree = ""; }; 00B2AC6421FD1A4500CBEC8E /* migrations */ = {isa = PBXFileReference; lastKnownFileType = folder; name = migrations; path = ../core/assets/main/migrations; sourceTree = ""; }; 00B2AC6621FD1CEF00CBEC8E /* IosFilesTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosFilesTest.swift; sourceTree = ""; }; 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = "../node_modules/react-native/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj"; sourceTree = ""; }; @@ -373,6 +371,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 00513C3B2200843F00702112 /* libthird-party.a in Frameworks */, + 00513C3C2200843F00702112 /* libyoga.a in Frameworks */, 000BCE0521F6CB1100F4DA11 /* libRCTWebSocket.a in Frameworks */, 000C283821F51C9B00C5EC6D /* libRNSVG.a in Frameworks */, ADBDB9381DFEBF1600ED6528 /* libRCTBlob.a in Frameworks */, @@ -425,8 +425,8 @@ isa = PBXGroup; children = ( 002101A521F936A300F9283D /* Info.plist */, - 002101A321F936A300F9283D /* IosSqlDatabaseTest.swift */, 00B2AC6621FD1CEF00CBEC8E /* IosFilesTest.swift */, + 002101A321F936A300F9283D /* IosSqlDatabaseTest.swift */, ); name = "Unit Tests"; path = uhabitsTest; @@ -478,16 +478,14 @@ 13B07FAE1A68108700A75B9A /* Application */ = { isa = PBXGroup; children = ( - 13B07FB61A68108700A75B9A /* Info.plist */, - 13B07FB51A68108700A75B9A /* Images.xcassets */, - 13B07FB11A68108700A75B9A /* LaunchScreen.xib */, 0008A5F721F17531000DB3E7 /* BridgingHeader.h */, 0008A62B21F2B755000DB3E7 /* CoreModuleBridge.m */, + 13B07FB61A68108700A75B9A /* Info.plist */, 0008A5F521F17513000DB3E7 /* AppDelegate.swift */, 0008A62921F2B728000DB3E7 /* CoreModule.swift */, 0021019B21F8AA3E00F9283D /* IosDatabase.swift */, - 00B2AC3C21FCA9D900CBEC8E /* IosFiles.swift */, - 0091878421FD70B5001BDE6B /* IosLog.swift */, + 13B07FB51A68108700A75B9A /* Images.xcassets */, + 13B07FB11A68108700A75B9A /* LaunchScreen.xib */, ); name = Application; sourceTree = ""; @@ -545,11 +543,8 @@ 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( - 000BCDF621F6CAFF00F4DA11 /* RCTWebSocket.xcodeproj */, - 000C280A21F51C4E00C5EC6D /* RNSVG.xcodeproj */, - 5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */, - 146833FF1AC3E56700842450 /* React.xcodeproj */, 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */, + 5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */, ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */, 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */, 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */, @@ -557,6 +552,9 @@ 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */, 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */, 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */, + 000BCDF621F6CAFF00F4DA11 /* RCTWebSocket.xcodeproj */, + 146833FF1AC3E56700842450 /* React.xcodeproj */, + 000C280A21F51C4E00C5EC6D /* RNSVG.xcodeproj */, ); name = Libraries; sourceTree = ""; @@ -1055,9 +1053,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 00B2AC3D21FCA9D900CBEC8E /* IosFiles.swift in Sources */, 0008A62C21F2B755000DB3E7 /* CoreModuleBridge.m in Sources */, - 0091878521FD70B5001BDE6B /* IosLog.swift in Sources */, 0021019C21F8AA3E00F9283D /* IosDatabase.swift in Sources */, 0008A62A21F2B728000DB3E7 /* CoreModule.swift in Sources */, 0008A5F621F17513000DB3E7 /* AppDelegate.swift in Sources */, diff --git a/ios/uhabits/AppDelegate.swift b/ios/uhabits/AppDelegate.swift index 3e78dfcb0..461d6398a 100644 --- a/ios/uhabits/AppDelegate.swift +++ b/ios/uhabits/AppDelegate.swift @@ -25,9 +25,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? var bridge: RCTBridge! - static var backend = Backend(databaseOpener: IosDatabaseOpener(), + static var backend = Backend(databaseOpener: IosDatabaseOpener(withLog: StandardLog()), fileOpener: IosFileOpener(), - log: IosLog()) + log: StandardLog()) func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let jsCodeLocation = RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index.ios", fallbackResource: nil) diff --git a/ios/uhabits/IosDatabase.swift b/ios/uhabits/IosDatabase.swift index 6a9062877..bb629b44b 100644 --- a/ios/uhabits/IosDatabase.swift +++ b/ios/uhabits/IosDatabase.swift @@ -24,7 +24,6 @@ internal let SQLITE_STATIC = unsafeBitCast(0, to: sqlite3_destructor_type.self) internal let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) class IosPreparedStatement : NSObject, PreparedStatement { - var db: OpaquePointer var statement: OpaquePointer @@ -53,12 +52,20 @@ class IosPreparedStatement : NSObject, PreparedStatement { return String(cString: sqlite3_column_text(statement, index)) } + func getReal(index: Int32) -> Double { + return sqlite3_column_double(statement, index) + } + func bindInt(index: Int32, value: Int32) { - sqlite3_bind_int(statement, index, value) + sqlite3_bind_int(statement, index + 1, value) } func bindText(index: Int32, value: String) { - sqlite3_bind_text(statement, index, value, -1, SQLITE_TRANSIENT) + sqlite3_bind_text(statement, index + 1, value, -1, SQLITE_TRANSIENT) + } + + func bindReal(index: Int32, value: Double) { + sqlite3_bind_double(statement, index + 1, value) } func reset() { @@ -72,16 +79,18 @@ class IosPreparedStatement : NSObject, PreparedStatement { class IosDatabase : NSObject, Database { var db: OpaquePointer + var log: Log - init(withDb db: OpaquePointer) { + init(withDb db: OpaquePointer, withLog log: Log) { self.db = db + self.log = log } func prepareStatement(sql: String) -> PreparedStatement { if sql.isEmpty { fatalError("Provided SQL query is empty") } - print("Running SQL: \(sql)") + log.debug(tag: "IosDatabase", msg: "Preparing: \(sql)") var statement : OpaquePointer? let result = sqlite3_prepare_v2(db, sql, -1, &statement, nil) if result == SQLITE_OK { @@ -98,16 +107,23 @@ class IosDatabase : NSObject, Database { } class IosDatabaseOpener : NSObject, DatabaseOpener { + + var log: Log + + init(withLog log: Log) { + self.log = log + } + func open(file: UserFile) -> Database { let dbPath = (file as! IosUserFile).path let version = String(cString: sqlite3_libversion()) - print("SQLite \(version)") - print("Opening database: \(dbPath)") + log.info(tag: "IosDatabaseOpener", msg: "SQLite \(version)") + log.info(tag: "IosDatabaseOpener", msg: "Opening database: \(dbPath)") var db: OpaquePointer? let result = sqlite3_open(dbPath, &db) if result == SQLITE_OK { - return IosDatabase(withDb: db!) + return IosDatabase(withDb: db!, withLog: log) } else { fatalError("Error opening database (code \(result))") } diff --git a/ios/uhabits/IosFiles.swift b/ios/uhabits/IosFiles.swift deleted file mode 100644 index 5ed33e78b..000000000 --- a/ios/uhabits/IosFiles.swift +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2016-2019 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -import Foundation - -class IosResourceFile : NSObject, ResourceFile { - - var path: String - - var fileManager = FileManager.default - - init(forPath path: String) { - self.path = path - } - - func readLines() -> [String] { - do { - let contents = try String(contentsOfFile: self.path, encoding: .utf8) - return contents.components(separatedBy: CharacterSet.newlines) - } catch { - return [""] - } - } -} - -class IosUserFile : NSObject, UserFile { - - var path: String - - init(forPath path: String) { - self.path = path - } - - func delete() { - do { - try FileManager.default.removeItem(atPath: path) - } catch { - - } - } - - func exists() -> Bool { - return FileManager.default.fileExists(atPath: path) - } -} - -class IosFileOpener : NSObject, FileOpener { - func openResourceFile(filename: String) -> ResourceFile { - let path = "\(Bundle.main.resourcePath!)/\(filename)" - return IosResourceFile(forPath: path) - } - - func openUserFile(filename: String) -> UserFile { - do { - let root = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).path - return IosUserFile(forPath: "\(root)/\(filename)") - } catch { - return IosUserFile(forPath: "invalid") - } - } -} diff --git a/ios/uhabitsTest/IosFilesTest.swift b/ios/uhabitsTest/IosFilesTest.swift index 55b9a3fa4..a7a19eb16 100644 --- a/ios/uhabitsTest/IosFilesTest.swift +++ b/ios/uhabitsTest/IosFilesTest.swift @@ -32,6 +32,7 @@ class IosFilesTest: XCTestCase { let fm = FileManager.default let root = try fm.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).path let path = "\(root)/test.txt" + print(path) fm.createFile(atPath: path, contents: "Hello world\nThis is line 2".data(using: .utf8), attributes: nil) let fileOpener = IosFileOpener() diff --git a/ios/uhabitsTest/IosSqlDatabaseTest.swift b/ios/uhabitsTest/IosSqlDatabaseTest.swift index 0093dfeda..37c7c4034 100644 --- a/ios/uhabitsTest/IosSqlDatabaseTest.swift +++ b/ios/uhabitsTest/IosSqlDatabaseTest.swift @@ -22,7 +22,7 @@ import XCTest class IosDatabaseTest: XCTestCase { func testUsage() { - let databaseOpener = IosDatabaseOpener() + let databaseOpener = IosDatabaseOpener(withLog: StandardLog()) let fileOpener = IosFileOpener() let dbFile = fileOpener.openUserFile(filename: "test.sqlite3") diff --git a/react-native/.eslintrc.js b/react-native/.eslintrc.js index cefef93ce..926390b82 100644 --- a/react-native/.eslintrc.js +++ b/react-native/.eslintrc.js @@ -3,5 +3,6 @@ module.exports = { "rules": { "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], "import/prefer-default-export": false, + "react/prefer-stateless-function": false, } }; diff --git a/react-native/index.ios.js b/react-native/index.ios.js index 9218a2f2d..6bae32bfd 100644 --- a/react-native/index.ios.js +++ b/react-native/index.ios.js @@ -21,23 +21,45 @@ import React from 'react'; import { AppRegistry, NavigatorIOS, - NativeModules, - NativeEventEmitter, } from 'react-native'; import ListHabitsScene from './src/components/ListHabits/index'; +import EditHabitScene from './src/components/EditHabit/index'; -function RootComponent() { - return ( - - ); +let navigator; + +const routes = { + index: { + component: ListHabitsScene, + title: 'Habits', + rightButtonSystemIcon: 'add', + onRightButtonPress: () => navigator.push(routes.newHabit), + passProps: { + onClickHabit: () => navigator.push(routes.newHabit), + onClickCheckmark: () => {}, + }, + }, + newHabit: { + component: EditHabitScene, + title: 'New Habit', + leftButtonTitle: 'Cancel', + rightButtonTitle: 'Save', + onLeftButtonPress: () => navigator.pop(), + onRightButtonPress: () => navigator.pop(), + }, +}; + + +class RootComponent extends React.Component { + render() { + return ( + { navigator = c; }} + translucent={false} + initialRoute={routes.index} + style={{ flex: 1 }} + /> + ); + } } AppRegistry.registerComponent('LoopHabitTracker', () => RootComponent); diff --git a/react-native/package-lock.json b/react-native/package-lock.json index 2488dbdd1..f0835a7c3 100644 --- a/react-native/package-lock.json +++ b/react-native/package-lock.json @@ -4910,38 +4910,6 @@ "color": "^2.0.1" } }, - "react-native-vector-icons": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-6.1.0.tgz", - "integrity": "sha512-1GF5I4VWgwnzBtVfAKNgEiR5ziHi5QaKL381wwApMzuiFgIJMNt5XIChuKwKoaiB86s+P5iMcYWxYCyENL96lA==", - "requires": { - "lodash": "^4.0.0", - "prop-types": "^15.6.2", - "yargs": "^8.0.2" - }, - "dependencies": { - "yargs": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz", - "integrity": "sha1-YpmpBVsc78lp/355wdkY3Osiw2A=", - "requires": { - "camelcase": "^4.1.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^2.0.0", - "read-pkg-up": "^2.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^7.0.0" - } - } - } - }, "react-proxy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/react-proxy/-/react-proxy-1.1.8.tgz", diff --git a/react-native/package.json b/react-native/package.json index ce7146343..72507a4b1 100644 --- a/react-native/package.json +++ b/react-native/package.json @@ -9,8 +9,7 @@ "prop-types": "^15.6.2", "react": "^16.6.3", "react-native": "^0.57.8", - "react-native-svg": "^9.0.0", - "react-native-vector-icons": "^6.1.0" + "react-native-svg": "^9.0.0" }, "devDependencies": { "eslint": "^5.12.1", diff --git a/react-native/src/components/EditHabit/index.js b/react-native/src/components/EditHabit/index.js new file mode 100644 index 000000000..bb35cbd47 --- /dev/null +++ b/react-native/src/components/EditHabit/index.js @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +import React from 'react'; +import { + StyleSheet, + TextInput, + View, + Text, + ScrollView, + TouchableOpacity, + TouchableHighlight, +} from 'react-native'; +import FontAwesome from '../../helpers/FontAwesome'; +import { Colors } from '../../helpers/Colors'; +import ColorCircle from '../common/ColorCircle'; + +const styles = StyleSheet.create({ + container: { + backgroundColor: Colors.appBackground, + flex: 1, + }, + item: { + fontSize: 17, + paddingTop: 15, + paddingBottom: 15, + paddingRight: 15, + paddingLeft: 15, + flex: 1, + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#fff', + }, + label: { + fontSize: 17, + flex: 1, + }, + value: { + fontSize: 17, + }, + multiline: { + }, + middle: { + borderBottomColor: Colors.headerBorderColor, + borderBottomWidth: StyleSheet.hairlineWidth, + }, + section: { + backgroundColor: Colors.appBackground, + marginTop: 30, + borderTopColor: Colors.headerBorderColor, + borderTopWidth: StyleSheet.hairlineWidth, + borderBottomColor: Colors.headerBorderColor, + borderBottomWidth: StyleSheet.hairlineWidth, + }, + icon: { + fontFamily: 'FontAwesome', + color: Colors.unchecked, + marginLeft: 10, + fontSize: 12, + paddingTop: 2, + }, + text: { + borderWidth: 1, + padding: 25, + backgroundColor: '#fff', + }, +}); + +export default class EditHabitsScene extends React.Component { + render() { + return ( + + + + + + + {}}> + + Color + + {FontAwesome.chevronRight} + + + {}}> + + Repeat + Every Day + {FontAwesome.chevronRight} + + + {}}> + + Reminder + 12:30 + {FontAwesome.chevronRight} + + + + + ); + } +} diff --git a/react-native/src/components/ListHabits/CheckmarkButton.js b/react-native/src/components/ListHabits/CheckmarkButton.js index 70f2c6b87..419eb1d34 100644 --- a/react-native/src/components/ListHabits/CheckmarkButton.js +++ b/react-native/src/components/ListHabits/CheckmarkButton.js @@ -29,14 +29,14 @@ import { Colors } from '../../helpers/Colors'; const styles = StyleSheet.create({ checkmarkBox: { - width: 44, - height: 44, + width: 55, + height: 55, justifyContent: 'center', alignItems: 'center', }, checkmark: { fontFamily: 'FontAwesome', - fontSize: 14, + fontSize: 17, }, }); diff --git a/react-native/src/components/ListHabits/HabitList.js b/react-native/src/components/ListHabits/HabitList.js index 78b8a569a..d31416f5e 100644 --- a/react-native/src/components/ListHabits/HabitList.js +++ b/react-native/src/components/ListHabits/HabitList.js @@ -17,12 +17,15 @@ * with this program. If not, see . */ +import PropTypes from 'prop-types'; import React from 'react'; import { FlatList, StyleSheet, Text, View, + TouchableHighlight, + TouchableOpacity, } from 'react-native'; import { Colors } from '../../helpers/Colors'; import { Emitter, Backend } from '../../helpers/Backend'; @@ -32,23 +35,19 @@ import CheckmarkButton from './CheckmarkButton'; const styles = StyleSheet.create({ item: { backgroundColor: Colors.itemBackground, - padding: 1, - marginTop: 0, - marginBottom: 1, - marginLeft: 0, - marginRight: 0, - elevation: 0, + borderBottomColor: Colors.headerBorderColor, + borderBottomWidth: StyleSheet.hairlineWidth, flexDirection: 'row', alignItems: 'stretch', }, ringContainer: { - width: 35, - height: 45, + width: 40, + height: 55, justifyContent: 'center', alignItems: 'center', }, labelContainer: { - width: 44, + width: 1, flex: 1, justifyContent: 'center', }, @@ -69,38 +68,51 @@ export default class HabitList extends React.Component { render() { const { habits } = this.state; + const { onClickHabit, onClickCheckmark } = this.props; return ( ( - - - + onClickHabit(item.key)}> + + + + + + + {item.name} + + + onClickCheckmark(item.key, 1)}> + + + onClickCheckmark(item.key, 2)}> + + + onClickCheckmark(item.key, 3)}> + + - - - {item.name} - - - - - - - + )} /> ); } } + +HabitList.propTypes = { + onClickHabit: PropTypes.func.isRequired, + onClickCheckmark: PropTypes.func.isRequired, +}; diff --git a/react-native/src/components/ListHabits/HabitListHeader.js b/react-native/src/components/ListHabits/HabitListHeader.js index 39b680d9a..44d93423c 100644 --- a/react-native/src/components/ListHabits/HabitListHeader.js +++ b/react-native/src/components/ListHabits/HabitListHeader.js @@ -24,7 +24,7 @@ import { Colors } from '../../helpers/Colors'; const styles = StyleSheet.create({ container: { - height: 50, + height: 55, paddingRight: 1, backgroundColor: Colors.headerBackground, flexDirection: 'row', @@ -35,7 +35,7 @@ const styles = StyleSheet.create({ borderBottomWidth: StyleSheet.hairlineWidth, }, column: { - width: 44, + width: 55, alignItems: 'center', }, text: { @@ -43,7 +43,7 @@ const styles = StyleSheet.create({ fontWeight: 'bold', }, dayName: { - fontSize: 10, + fontSize: 12, }, dayNumber: { fontSize: 12, @@ -81,11 +81,6 @@ export default class HabitListHeader extends React.Component { dayName: 'Thu', dayNumber: '3', }, - { - dayName: 'Wed', - dayNumber: '2', - }, - ].map((day) => { const { dayName, dayNumber } = day; return HabitListHeader.renderColumn(dayName, dayNumber); diff --git a/react-native/src/components/ListHabits/index.js b/react-native/src/components/ListHabits/index.js index 7565d1762..c92da2219 100644 --- a/react-native/src/components/ListHabits/index.js +++ b/react-native/src/components/ListHabits/index.js @@ -17,6 +17,7 @@ * with this program. If not, see . */ +import PropTypes from 'prop-types'; import React from 'react'; import { StyleSheet, View } from 'react-native'; import { Colors } from '../../helpers/Colors'; @@ -30,11 +31,22 @@ const styles = StyleSheet.create({ }, }); -export default function ListHabitsScene() { - return ( - - - - - ); +export default class ListHabitsScene extends React.Component { + render() { + const { onClickHabit, onClickCheckmark } = this.props; + return ( + + + + + ); + } } + +ListHabitsScene.propTypes = { + onClickHabit: PropTypes.func.isRequired, + onClickCheckmark: PropTypes.func.isRequired, +}; diff --git a/ios/uhabits/IosLog.swift b/react-native/src/components/common/ColorCircle.js similarity index 58% rename from ios/uhabits/IosLog.swift rename to react-native/src/components/common/ColorCircle.js index 94f7fea0d..b117ffeae 100644 --- a/ios/uhabits/IosLog.swift +++ b/react-native/src/components/common/ColorCircle.js @@ -17,14 +17,22 @@ * with this program. If not, see . */ -import Foundation +import React from 'react'; +import PropTypes from 'prop-types'; +import Svg, { Circle } from 'react-native-svg'; +import { Colors } from '../../helpers/Colors'; -class IosLog : NSObject, Log { - func info(msg: String) { - print("[I] \(msg)") - } - - func debug(msg: String) { - print("[D] \(msg)") - } +export default function ColorCircle(props) { + const { size, color } = props; + return ( + + + + + ); } + +ColorCircle.propTypes = { + size: PropTypes.number.isRequired, + color: PropTypes.string.isRequired, +}; diff --git a/react-native/src/helpers/FontAwesome.js b/react-native/src/helpers/FontAwesome.js index cee76b6d0..5f66a1c5e 100644 --- a/react-native/src/helpers/FontAwesome.js +++ b/react-native/src/helpers/FontAwesome.js @@ -1,4 +1,5 @@ module.exports = { check: '\uf00c', times: '\uf00d', + chevronRight: '\uf054', };