pull/498/head
Alinson S. Xavier 7 years ago
parent 7cab0a39e5
commit a546f6de73

@ -19,26 +19,27 @@
package org.isoron.uhabits package org.isoron.uhabits
import org.isoron.uhabits.models.HabitList import org.isoron.uhabits.models.*
import org.isoron.uhabits.utils.* import org.isoron.uhabits.utils.*
class Backend(var databaseOpener: DatabaseOpener, class Backend(var databaseOpener: DatabaseOpener,
var fileOpener: FileOpener, var fileOpener: FileOpener,
var log: Log) { var log: Log) {
var db: Database var database: Database
var habitsRepository: HabitRepository
var habits: HabitList var habits = mutableMapOf<Int, Habit>()
init { init {
val dbFile = fileOpener.openUserFile("uhabits.sqlite3") val dbFile = fileOpener.openUserFile("uhabits.db")
db = databaseOpener.open(dbFile) database = databaseOpener.open(dbFile)
db.migrateTo(LOOP_DATABASE_VERSION, fileOpener, log) database.migrateTo(LOOP_DATABASE_VERSION, fileOpener, log)
habits = HabitList(db) habitsRepository = HabitRepository(database)
habits = habitsRepository.findAll()
} }
fun getHabitList(): List<Map<String, *>> { fun getHabitList(): List<Map<String, *>> {
return habits.getActive().map { h -> return habits.values.sortedBy { h -> h.position }.map { h ->
mapOf("key" to h.id.toString(), mapOf("key" to h.id.toString(),
"name" to h.name, "name" to h.name,
"color" to h.color.paletteIndex) "color" to h.color.paletteIndex)
@ -46,11 +47,30 @@ class Backend(var databaseOpener: DatabaseOpener,
} }
fun createHabit(name: String) { 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) { fun deleteHabit(id: Int) {
val habit = habits[id]!!
habitsRepository.delete(habit)
habits.remove(id)
} }
fun updateHabit(id: Int, name: String) { fun updateHabit(id: Int, name: String) {
val habit = habits[id]!!
habit.name = name
habitsRepository.update(habit)
} }
} }

@ -19,4 +19,4 @@
package org.isoron.uhabits.models package org.isoron.uhabits.models
class Color(val paletteIndex: Int) data class Color(val paletteIndex: Int)

@ -27,5 +27,5 @@ data class Habit(var id: Int,
var isArchived: Boolean, var isArchived: Boolean,
var position: Int, var position: Int,
var unit: String, var unit: String,
var target: Int, var target: Double,
var type: HabitType) var type: HabitType)

@ -1,57 +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.uhabits.utils.Database
import org.isoron.uhabits.utils.StepResult
class HabitList(var db: Database) {
var habits = mutableListOf<Habit>()
init {
loadHabitsFromDatabase()
}
fun getActive(): List<Habit> {
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()
}
}

@ -0,0 +1,99 @@
/*
* 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.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<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 = 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()
}
}

@ -19,7 +19,7 @@
package org.isoron.uhabits.models package org.isoron.uhabits.models
enum class HabitType { enum class HabitType(val code: Int) {
YES_NO_HABIT, YES_NO_HABIT(0),
NUMERICAL_HABIT, NUMERICAL_HABIT(1),
} }

@ -29,8 +29,10 @@ interface PreparedStatement {
fun finalize() fun finalize()
fun getInt(index: Int): Int fun getInt(index: Int): Int
fun getText(index: Int): String fun getText(index: Int): String
fun getReal(index: Int): Double
fun bindInt(index: Int, value: Int) fun bindInt(index: Int, value: Int)
fun bindText(index: Int, value: String) fun bindText(index: Int, value: String)
fun bindReal(index: Int, value: Double)
fun reset() fun reset()
} }
@ -57,25 +59,38 @@ fun Database.queryInt(sql: String): Int {
return result 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.begin() = execute("begin")
fun Database.commit() = execute("commit") fun Database.commit() = execute("commit")
fun Database.rollback() = execute("rollback")
fun Database.getVersion() = queryInt("pragma user_version") fun Database.getVersion() = queryInt("pragma user_version")
fun Database.setVersion(v: Int) = execute("pragma user_version = $v") fun Database.setVersion(v: Int) = execute("pragma user_version = $v")
fun Database.migrateTo(newVersion: Int, fileOpener: FileOpener, log: Log) { fun Database.migrateTo(newVersion: Int, fileOpener: FileOpener, log: Log) {
val currentVersion = getVersion() val currentVersion = getVersion()
log.debug("Current database version: $currentVersion") log.debug("Database", "Current database version: $currentVersion")
if (currentVersion == newVersion) return if (currentVersion == newVersion) return
log.debug("Upgrading to version: $newVersion") log.debug("Database", "Upgrading to version: $newVersion")
if (currentVersion > newVersion) if (currentVersion > newVersion)
throw RuntimeException("database produced by future version of the application") throw RuntimeException("database produced by future version of the application")
begin() begin()
for (v in (currentVersion + 1)..newVersion) { 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 filename = sprintf("migrations/%03d.sql", v)
val migrationFile = fileOpener.openResourceFile(filename) val migrationFile = fileOpener.openResourceFile(filename)
for (line in migrationFile.readLines()) { for (line in migrationFile.readLines()) {

@ -20,6 +20,20 @@
package org.isoron.uhabits.utils package org.isoron.uhabits.utils
interface Log { interface Log {
fun info(msg: String) fun info(tag: String, msg: String)
fun debug(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))
}
} }

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

@ -17,17 +17,16 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.uhabits.database package org.isoron.uhabits.utils
import org.isoron.uhabits.utils.*
import java.sql.Connection import java.sql.Connection
import java.sql.DriverManager import java.sql.DriverManager
import java.sql.PreparedStatement import java.sql.PreparedStatement
import java.sql.ResultSet 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 rs: ResultSet? = null
private var hasExecuted = false private var hasExecuted = false
override fun step(): StepResult { 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 if (rs == null || !rs!!.next()) return StepResult.DONE
return StepResult.ROW return StepResult.ROW
} }
override fun finalize() { override fun finalize() {
stmt.close() stmt.close()
} }
@ -53,16 +51,25 @@ class JavaPreparedStatement(private var stmt : PreparedStatement) : org.isoron.u
return rs!!.getString(index + 1) return rs!!.getString(index + 1)
} }
override fun getReal(index: Int): Double {
return rs!!.getDouble(index + 1)
}
override fun bindInt(index: Int, value: Int) { override fun bindInt(index: Int, value: Int) {
stmt.setInt(index, value) stmt.setInt(index + 1, value)
} }
override fun bindText(index: Int, value: String) { 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() { override fun reset() {
stmt.clearParameters() stmt.clearParameters()
hasExecuted = false
} }
} }
@ -70,7 +77,7 @@ class JavaDatabase(private var conn: Connection,
private val log: Log) : Database { private val log: Log) : Database {
override fun prepareStatement(sql: String): org.isoron.uhabits.utils.PreparedStatement { 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)) return JavaPreparedStatement(conn.prepareStatement(sql))
} }
override fun close() { override fun close() {

@ -19,11 +19,21 @@
package org.isoron.uhabits package org.isoron.uhabits
import org.isoron.uhabits.utils.JavaFileOpener import org.isoron.uhabits.models.HabitRepository
import org.isoron.uhabits.database.JavaDatabaseOpener import org.isoron.uhabits.utils.*
import org.isoron.uhabits.utils.JavaLog import org.junit.Before
open class BaseTest { open class BaseTest {
val fileOpener = JavaFileOpener() 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)
}
} }

@ -26,15 +26,6 @@ import org.junit.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
class JavaDatabaseTest : BaseTest() { 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 @Test
fun testUsage() { fun testUsage() {
db.setVersion(0) db.setVersion(0)
@ -51,14 +42,14 @@ class JavaDatabaseTest : BaseTest() {
stmt.step() stmt.step()
stmt.finalize() stmt.finalize()
stmt = db.prepareStatement("insert into demo(key, value) values (?1, ?2)") stmt = db.prepareStatement("insert into demo(key, value) values (?, ?)")
stmt.bindInt(1, 42) stmt.bindInt(0, 42)
stmt.bindText(2, "Hello World") stmt.bindText(1, "Hello World")
stmt.step() stmt.step()
stmt.finalize() stmt.finalize()
stmt = db.prepareStatement("select * from demo where key > ?1") stmt = db.prepareStatement("select * from demo where key > ?")
stmt.bindInt(1, 10) stmt.bindInt(0, 10)
var result = stmt.step() var result = stmt.step()
assertEquals(result, StepResult.ROW) assertEquals(result, StepResult.ROW)
@ -71,12 +62,4 @@ class JavaDatabaseTest : BaseTest() {
stmt.finalize() stmt.finalize()
db.close() db.close()
} }
@Test
fun testMigrateTo() {
assertEquals(0, db.getVersion())
db.migrateTo(22, fileOpener)
assertEquals(22, db.getVersion())
db.execute("select * from habits")
}
} }

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

@ -17,14 +17,23 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.uhabits.utils import UIKit
class JavaLog : Log { @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate {
override fun info(msg: String) {
println("[I] $msg") var window: UIWindow?
}
override fun debug(msg: String) { func application(_ application: UIApplication,
println("[D] $msg") didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
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
} }
} }

@ -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"
}
}

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

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

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UILaunchStoryboardName</key>
<string>Launch.storyboard</string>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13142" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12042"/>
</dependencies>
<scenes/>
</document>

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

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

@ -18,8 +18,8 @@
0021019C21F8AA3E00F9283D /* IosDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0021019B21F8AA3E00F9283D /* IosDatabase.swift */; }; 0021019C21F8AA3E00F9283D /* IosDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0021019B21F8AA3E00F9283D /* IosDatabase.swift */; };
002101A421F936A300F9283D /* IosSqlDatabaseTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 002101A321F936A300F9283D /* IosSqlDatabaseTest.swift */; }; 002101A421F936A300F9283D /* IosSqlDatabaseTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 002101A321F936A300F9283D /* IosSqlDatabaseTest.swift */; };
002101AC21F9428C00F9283D /* core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0008A5C021F16D25000DB3E7 /* core.framework */; }; 002101AC21F9428C00F9283D /* core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0008A5C021F16D25000DB3E7 /* core.framework */; };
0091878521FD70B5001BDE6B /* IosLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0091878421FD70B5001BDE6B /* IosLog.swift */; }; 00513C3B2200843F00702112 /* libthird-party.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DF0FFE32056DD460020B375 /* libthird-party.a */; };
00B2AC3D21FCA9D900CBEC8E /* IosFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00B2AC3C21FCA9D900CBEC8E /* IosFiles.swift */; }; 00513C3C2200843F00702112 /* libyoga.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DAD3EA51DF850E9000B6D8A /* libyoga.a */; };
00B2AC6521FD1A4500CBEC8E /* migrations in Resources */ = {isa = PBXBuildFile; fileRef = 00B2AC6421FD1A4500CBEC8E /* migrations */; }; 00B2AC6521FD1A4500CBEC8E /* migrations in Resources */ = {isa = PBXBuildFile; fileRef = 00B2AC6421FD1A4500CBEC8E /* migrations */; };
00B2AC6821FD1DA700CBEC8E /* IosFilesTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00B2AC6621FD1CEF00CBEC8E /* IosFilesTest.swift */; }; 00B2AC6821FD1DA700CBEC8E /* IosFilesTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00B2AC6621FD1CEF00CBEC8E /* IosFilesTest.swift */; };
00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; }; 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; }; 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 = "<group>"; }; 002101A321F936A300F9283D /* IosSqlDatabaseTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosSqlDatabaseTest.swift; sourceTree = "<group>"; };
002101A521F936A300F9283D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 002101A521F936A300F9283D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
0091878421FD70B5001BDE6B /* IosLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = IosLog.swift; path = uhabits/IosLog.swift; sourceTree = "<group>"; };
00B2AC3C21FCA9D900CBEC8E /* IosFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = IosFiles.swift; path = uhabits/IosFiles.swift; sourceTree = "<group>"; };
00B2AC6421FD1A4500CBEC8E /* migrations */ = {isa = PBXFileReference; lastKnownFileType = folder; name = migrations; path = ../core/assets/main/migrations; sourceTree = "<group>"; }; 00B2AC6421FD1A4500CBEC8E /* migrations */ = {isa = PBXFileReference; lastKnownFileType = folder; name = migrations; path = ../core/assets/main/migrations; sourceTree = "<group>"; };
00B2AC6621FD1CEF00CBEC8E /* IosFilesTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosFilesTest.swift; sourceTree = "<group>"; }; 00B2AC6621FD1CEF00CBEC8E /* IosFilesTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosFilesTest.swift; sourceTree = "<group>"; };
00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = "../node_modules/react-native/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj"; sourceTree = "<group>"; }; 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = "../node_modules/react-native/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj"; sourceTree = "<group>"; };
@ -373,6 +371,8 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
00513C3B2200843F00702112 /* libthird-party.a in Frameworks */,
00513C3C2200843F00702112 /* libyoga.a in Frameworks */,
000BCE0521F6CB1100F4DA11 /* libRCTWebSocket.a in Frameworks */, 000BCE0521F6CB1100F4DA11 /* libRCTWebSocket.a in Frameworks */,
000C283821F51C9B00C5EC6D /* libRNSVG.a in Frameworks */, 000C283821F51C9B00C5EC6D /* libRNSVG.a in Frameworks */,
ADBDB9381DFEBF1600ED6528 /* libRCTBlob.a in Frameworks */, ADBDB9381DFEBF1600ED6528 /* libRCTBlob.a in Frameworks */,
@ -425,8 +425,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
002101A521F936A300F9283D /* Info.plist */, 002101A521F936A300F9283D /* Info.plist */,
002101A321F936A300F9283D /* IosSqlDatabaseTest.swift */,
00B2AC6621FD1CEF00CBEC8E /* IosFilesTest.swift */, 00B2AC6621FD1CEF00CBEC8E /* IosFilesTest.swift */,
002101A321F936A300F9283D /* IosSqlDatabaseTest.swift */,
); );
name = "Unit Tests"; name = "Unit Tests";
path = uhabitsTest; path = uhabitsTest;
@ -478,16 +478,14 @@
13B07FAE1A68108700A75B9A /* Application */ = { 13B07FAE1A68108700A75B9A /* Application */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
13B07FB61A68108700A75B9A /* Info.plist */,
13B07FB51A68108700A75B9A /* Images.xcassets */,
13B07FB11A68108700A75B9A /* LaunchScreen.xib */,
0008A5F721F17531000DB3E7 /* BridgingHeader.h */, 0008A5F721F17531000DB3E7 /* BridgingHeader.h */,
0008A62B21F2B755000DB3E7 /* CoreModuleBridge.m */, 0008A62B21F2B755000DB3E7 /* CoreModuleBridge.m */,
13B07FB61A68108700A75B9A /* Info.plist */,
0008A5F521F17513000DB3E7 /* AppDelegate.swift */, 0008A5F521F17513000DB3E7 /* AppDelegate.swift */,
0008A62921F2B728000DB3E7 /* CoreModule.swift */, 0008A62921F2B728000DB3E7 /* CoreModule.swift */,
0021019B21F8AA3E00F9283D /* IosDatabase.swift */, 0021019B21F8AA3E00F9283D /* IosDatabase.swift */,
00B2AC3C21FCA9D900CBEC8E /* IosFiles.swift */, 13B07FB51A68108700A75B9A /* Images.xcassets */,
0091878421FD70B5001BDE6B /* IosLog.swift */, 13B07FB11A68108700A75B9A /* LaunchScreen.xib */,
); );
name = Application; name = Application;
sourceTree = "<group>"; sourceTree = "<group>";
@ -545,11 +543,8 @@
832341AE1AAA6A7D00B99B32 /* Libraries */ = { 832341AE1AAA6A7D00B99B32 /* Libraries */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
000BCDF621F6CAFF00F4DA11 /* RCTWebSocket.xcodeproj */,
000C280A21F51C4E00C5EC6D /* RNSVG.xcodeproj */,
5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */,
146833FF1AC3E56700842450 /* React.xcodeproj */,
00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */, 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */,
5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */,
ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */, ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */,
00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */, 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */,
00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */, 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */,
@ -557,6 +552,9 @@
00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */, 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */,
139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */, 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */,
832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */, 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */,
000BCDF621F6CAFF00F4DA11 /* RCTWebSocket.xcodeproj */,
146833FF1AC3E56700842450 /* React.xcodeproj */,
000C280A21F51C4E00C5EC6D /* RNSVG.xcodeproj */,
); );
name = Libraries; name = Libraries;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1055,9 +1053,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
00B2AC3D21FCA9D900CBEC8E /* IosFiles.swift in Sources */,
0008A62C21F2B755000DB3E7 /* CoreModuleBridge.m in Sources */, 0008A62C21F2B755000DB3E7 /* CoreModuleBridge.m in Sources */,
0091878521FD70B5001BDE6B /* IosLog.swift in Sources */,
0021019C21F8AA3E00F9283D /* IosDatabase.swift in Sources */, 0021019C21F8AA3E00F9283D /* IosDatabase.swift in Sources */,
0008A62A21F2B728000DB3E7 /* CoreModule.swift in Sources */, 0008A62A21F2B728000DB3E7 /* CoreModule.swift in Sources */,
0008A5F621F17513000DB3E7 /* AppDelegate.swift in Sources */, 0008A5F621F17513000DB3E7 /* AppDelegate.swift in Sources */,

@ -25,9 +25,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? var window: UIWindow?
var bridge: RCTBridge! var bridge: RCTBridge!
static var backend = Backend(databaseOpener: IosDatabaseOpener(), static var backend = Backend(databaseOpener: IosDatabaseOpener(withLog: StandardLog()),
fileOpener: IosFileOpener(), fileOpener: IosFileOpener(),
log: IosLog()) log: StandardLog())
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let jsCodeLocation = RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index.ios", fallbackResource: nil) let jsCodeLocation = RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index.ios", fallbackResource: nil)

@ -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) internal let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
class IosPreparedStatement : NSObject, PreparedStatement { class IosPreparedStatement : NSObject, PreparedStatement {
var db: OpaquePointer var db: OpaquePointer
var statement: OpaquePointer var statement: OpaquePointer
@ -53,12 +52,20 @@ class IosPreparedStatement : NSObject, PreparedStatement {
return String(cString: sqlite3_column_text(statement, index)) 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) { 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) { 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() { func reset() {
@ -72,16 +79,18 @@ class IosPreparedStatement : NSObject, PreparedStatement {
class IosDatabase : NSObject, Database { class IosDatabase : NSObject, Database {
var db: OpaquePointer var db: OpaquePointer
var log: Log
init(withDb db: OpaquePointer) { init(withDb db: OpaquePointer, withLog log: Log) {
self.db = db self.db = db
self.log = log
} }
func prepareStatement(sql: String) -> PreparedStatement { func prepareStatement(sql: String) -> PreparedStatement {
if sql.isEmpty { if sql.isEmpty {
fatalError("Provided SQL query is empty") fatalError("Provided SQL query is empty")
} }
print("Running SQL: \(sql)") log.debug(tag: "IosDatabase", msg: "Preparing: \(sql)")
var statement : OpaquePointer? var statement : OpaquePointer?
let result = sqlite3_prepare_v2(db, sql, -1, &statement, nil) let result = sqlite3_prepare_v2(db, sql, -1, &statement, nil)
if result == SQLITE_OK { if result == SQLITE_OK {
@ -98,16 +107,23 @@ class IosDatabase : NSObject, Database {
} }
class IosDatabaseOpener : NSObject, DatabaseOpener { class IosDatabaseOpener : NSObject, DatabaseOpener {
var log: Log
init(withLog log: Log) {
self.log = log
}
func open(file: UserFile) -> Database { func open(file: UserFile) -> Database {
let dbPath = (file as! IosUserFile).path let dbPath = (file as! IosUserFile).path
let version = String(cString: sqlite3_libversion()) let version = String(cString: sqlite3_libversion())
print("SQLite \(version)") log.info(tag: "IosDatabaseOpener", msg: "SQLite \(version)")
print("Opening database: \(dbPath)") log.info(tag: "IosDatabaseOpener", msg: "Opening database: \(dbPath)")
var db: OpaquePointer? var db: OpaquePointer?
let result = sqlite3_open(dbPath, &db) let result = sqlite3_open(dbPath, &db)
if result == SQLITE_OK { if result == SQLITE_OK {
return IosDatabase(withDb: db!) return IosDatabase(withDb: db!, withLog: log)
} else { } else {
fatalError("Error opening database (code \(result))") fatalError("Error opening database (code \(result))")
} }

@ -1,77 +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/>.
*/
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")
}
}
}

@ -32,6 +32,7 @@ class IosFilesTest: XCTestCase {
let fm = FileManager.default let fm = FileManager.default
let root = try fm.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).path let root = try fm.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).path
let path = "\(root)/test.txt" let path = "\(root)/test.txt"
print(path)
fm.createFile(atPath: path, contents: "Hello world\nThis is line 2".data(using: .utf8), attributes: nil) fm.createFile(atPath: path, contents: "Hello world\nThis is line 2".data(using: .utf8), attributes: nil)
let fileOpener = IosFileOpener() let fileOpener = IosFileOpener()

@ -22,7 +22,7 @@ import XCTest
class IosDatabaseTest: XCTestCase { class IosDatabaseTest: XCTestCase {
func testUsage() { func testUsage() {
let databaseOpener = IosDatabaseOpener() let databaseOpener = IosDatabaseOpener(withLog: StandardLog())
let fileOpener = IosFileOpener() let fileOpener = IosFileOpener()
let dbFile = fileOpener.openUserFile(filename: "test.sqlite3") let dbFile = fileOpener.openUserFile(filename: "test.sqlite3")

@ -3,5 +3,6 @@ module.exports = {
"rules": { "rules": {
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
"import/prefer-default-export": false, "import/prefer-default-export": false,
"react/prefer-stateless-function": false,
} }
}; };

@ -21,23 +21,45 @@ import React from 'react';
import { import {
AppRegistry, AppRegistry,
NavigatorIOS, NavigatorIOS,
NativeModules,
NativeEventEmitter,
} from 'react-native'; } from 'react-native';
import ListHabitsScene from './src/components/ListHabits/index'; import ListHabitsScene from './src/components/ListHabits/index';
import EditHabitScene from './src/components/EditHabit/index';
function RootComponent() { let navigator;
return (
<NavigatorIOS const routes = {
translucent={false} index: {
initialRoute={{
component: ListHabitsScene, component: ListHabitsScene,
title: 'Habits', title: 'Habits',
rightButtonSystemIcon: 'add', 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 (
<NavigatorIOS
ref={(c) => { navigator = c; }}
translucent={false}
initialRoute={routes.index}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
); );
}
} }
AppRegistry.registerComponent('LoopHabitTracker', () => RootComponent); AppRegistry.registerComponent('LoopHabitTracker', () => RootComponent);

@ -4910,38 +4910,6 @@
"color": "^2.0.1" "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": { "react-proxy": {
"version": "1.1.8", "version": "1.1.8",
"resolved": "https://registry.npmjs.org/react-proxy/-/react-proxy-1.1.8.tgz", "resolved": "https://registry.npmjs.org/react-proxy/-/react-proxy-1.1.8.tgz",

@ -9,8 +9,7 @@
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^16.6.3", "react": "^16.6.3",
"react-native": "^0.57.8", "react-native": "^0.57.8",
"react-native-svg": "^9.0.0", "react-native-svg": "^9.0.0"
"react-native-vector-icons": "^6.1.0"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^5.12.1", "eslint": "^5.12.1",

@ -0,0 +1,128 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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 (
<ScrollView style={styles.container}>
<View style={styles.section}>
<TextInput
autoFocus
style={[styles.item, styles.middle, { color: Colors[1] }]}
placeholder="Name"
/>
<TextInput
style={[styles.item]}
placeholder="Question (e.g. Did you exercise today?)"
multiline
/>
</View>
<View style={styles.section}>
<TouchableHighlight onPress={() => {}}>
<View style={[styles.item, styles.middle]}>
<Text style={styles.label}>Color</Text>
<ColorCircle size={20} color={Colors[1]} />
<Text style={styles.icon}>{FontAwesome.chevronRight}</Text>
</View>
</TouchableHighlight>
<TouchableHighlight onPress={() => {}}>
<View style={[styles.item, styles.middle]}>
<Text style={styles.label}>Repeat</Text>
<Text style={styles.value}>Every Day</Text>
<Text style={styles.icon}>{FontAwesome.chevronRight}</Text>
</View>
</TouchableHighlight>
<TouchableHighlight onPress={() => {}}>
<View style={[styles.item, styles.middle]}>
<Text style={styles.label}>Reminder</Text>
<Text style={styles.value}>12:30</Text>
<Text style={styles.icon}>{FontAwesome.chevronRight}</Text>
</View>
</TouchableHighlight>
</View>
</ScrollView>
);
}
}

@ -29,14 +29,14 @@ import { Colors } from '../../helpers/Colors';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
checkmarkBox: { checkmarkBox: {
width: 44, width: 55,
height: 44, height: 55,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
checkmark: { checkmark: {
fontFamily: 'FontAwesome', fontFamily: 'FontAwesome',
fontSize: 14, fontSize: 17,
}, },
}); });

@ -17,12 +17,15 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { import {
FlatList, FlatList,
StyleSheet, StyleSheet,
Text, Text,
View, View,
TouchableHighlight,
TouchableOpacity,
} from 'react-native'; } from 'react-native';
import { Colors } from '../../helpers/Colors'; import { Colors } from '../../helpers/Colors';
import { Emitter, Backend } from '../../helpers/Backend'; import { Emitter, Backend } from '../../helpers/Backend';
@ -32,23 +35,19 @@ import CheckmarkButton from './CheckmarkButton';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
item: { item: {
backgroundColor: Colors.itemBackground, backgroundColor: Colors.itemBackground,
padding: 1, borderBottomColor: Colors.headerBorderColor,
marginTop: 0, borderBottomWidth: StyleSheet.hairlineWidth,
marginBottom: 1,
marginLeft: 0,
marginRight: 0,
elevation: 0,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'stretch', alignItems: 'stretch',
}, },
ringContainer: { ringContainer: {
width: 35, width: 40,
height: 45, height: 55,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
labelContainer: { labelContainer: {
width: 44, width: 1,
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
}, },
@ -69,11 +68,13 @@ export default class HabitList extends React.Component {
render() { render() {
const { habits } = this.state; const { habits } = this.state;
const { onClickHabit, onClickCheckmark } = this.props;
return ( return (
<FlatList <FlatList
style={styles.container} style={styles.container}
data={habits} data={habits}
renderItem={({ item }) => ( renderItem={({ item }) => (
<TouchableHighlight onPress={() => onClickHabit(item.key)}>
<View style={styles.item}> <View style={styles.item}>
<View style={styles.ringContainer}> <View style={styles.ringContainer}>
<Ring <Ring
@ -87,20 +88,31 @@ export default class HabitList extends React.Component {
<Text <Text
numberOfLines={2} numberOfLines={2}
style={{ style={{
fontSize: 14, fontSize: 17,
color: Colors[item.color], color: Colors[item.color],
}} }}
> >
{item.name} {item.name}
</Text> </Text>
</View> </View>
<TouchableOpacity onPress={() => onClickCheckmark(item.key, 1)}>
<CheckmarkButton color={Colors[item.color]} /> <CheckmarkButton color={Colors[item.color]} />
</TouchableOpacity>
<TouchableOpacity onPress={() => onClickCheckmark(item.key, 2)}>
<CheckmarkButton color={Colors[item.color]} /> <CheckmarkButton color={Colors[item.color]} />
</TouchableOpacity>
<TouchableOpacity onPress={() => onClickCheckmark(item.key, 3)}>
<CheckmarkButton color={Colors[item.color]} /> <CheckmarkButton color={Colors[item.color]} />
<CheckmarkButton color={Colors[item.color]} /> </TouchableOpacity>
</View> </View>
</TouchableHighlight>
)} )}
/> />
); );
} }
} }
HabitList.propTypes = {
onClickHabit: PropTypes.func.isRequired,
onClickCheckmark: PropTypes.func.isRequired,
};

@ -24,7 +24,7 @@ import { Colors } from '../../helpers/Colors';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
height: 50, height: 55,
paddingRight: 1, paddingRight: 1,
backgroundColor: Colors.headerBackground, backgroundColor: Colors.headerBackground,
flexDirection: 'row', flexDirection: 'row',
@ -35,7 +35,7 @@ const styles = StyleSheet.create({
borderBottomWidth: StyleSheet.hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth,
}, },
column: { column: {
width: 44, width: 55,
alignItems: 'center', alignItems: 'center',
}, },
text: { text: {
@ -43,7 +43,7 @@ const styles = StyleSheet.create({
fontWeight: 'bold', fontWeight: 'bold',
}, },
dayName: { dayName: {
fontSize: 10, fontSize: 12,
}, },
dayNumber: { dayNumber: {
fontSize: 12, fontSize: 12,
@ -81,11 +81,6 @@ export default class HabitListHeader extends React.Component {
dayName: 'Thu', dayName: 'Thu',
dayNumber: '3', dayNumber: '3',
}, },
{
dayName: 'Wed',
dayNumber: '2',
},
].map((day) => { ].map((day) => {
const { dayName, dayNumber } = day; const { dayName, dayNumber } = day;
return HabitListHeader.renderColumn(dayName, dayNumber); return HabitListHeader.renderColumn(dayName, dayNumber);

@ -17,6 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import { Colors } from '../../helpers/Colors'; import { Colors } from '../../helpers/Colors';
@ -30,11 +31,22 @@ const styles = StyleSheet.create({
}, },
}); });
export default function ListHabitsScene() { export default class ListHabitsScene extends React.Component {
render() {
const { onClickHabit, onClickCheckmark } = this.props;
return ( return (
<View style={styles.container}> <View style={styles.container}>
<HabitListHeader /> <HabitListHeader />
<HabitList /> <HabitList
onClickHabit={onClickHabit}
onClickCheckmark={onClickCheckmark}
/>
</View> </View>
); );
}
} }
ListHabitsScene.propTypes = {
onClickHabit: PropTypes.func.isRequired,
onClickCheckmark: PropTypes.func.isRequired,
};

@ -17,14 +17,22 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
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 { export default function ColorCircle(props) {
func info(msg: String) { const { size, color } = props;
print("[I] \(msg)") return (
} <Svg height={size} width={size} viewBox="0 0 100 100">
<Circle cx={50} cy={50} r={50} fill={color} />
func debug(msg: String) { <Circle cx={50} cy={50} r={30} fill={Colors.itemBackground} />
print("[D] \(msg)") </Svg>
} );
} }
ColorCircle.propTypes = {
size: PropTypes.number.isRequired,
color: PropTypes.string.isRequired,
};

@ -1,4 +1,5 @@
module.exports = { module.exports = {
check: '\uf00c', check: '\uf00c',
times: '\uf00d', times: '\uf00d',
chevronRight: '\uf054',
}; };

Loading…
Cancel
Save