mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 01:08:50 -06:00
Update
This commit is contained in:
BIN
core/assets/main/databases/template.db
Normal file
BIN
core/assets/main/databases/template.db
Normal file
Binary file not shown.
@@ -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<Int, Habit>()
|
||||
|
||||
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<Map<String, *>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,4 +19,4 @@
|
||||
|
||||
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 position: Int,
|
||||
var unit: String,
|
||||
var target: Int,
|
||||
var target: Double,
|
||||
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
|
||||
|
||||
enum class HabitType {
|
||||
YES_NO_HABIT,
|
||||
NUMERICAL_HABIT,
|
||||
enum class HabitType(val code: Int) {
|
||||
YES_NO_HABIT(0),
|
||||
NUMERICAL_HABIT(1),
|
||||
}
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
55
core/src/iosMain/kotlin/org/isoron/uhabits/utils/IosFiles.kt
Normal file
55
core/src/iosMain/kotlin/org/isoron/uhabits/utils/IosFiles.kt
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.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/>.
|
||||
*/
|
||||
|
||||
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() {
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.utils
|
||||
import UIKit
|
||||
|
||||
class JavaLog : Log {
|
||||
override fun info(msg: String) {
|
||||
println("[I] $msg")
|
||||
}
|
||||
@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
override fun debug(msg: String) {
|
||||
println("[D] $msg")
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication,
|
||||
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"
|
||||
}
|
||||
}
|
||||
6
ios-native/Application/Assets.xcassets/Contents.json
Normal file
6
ios-native/Application/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
87
ios-native/Application/EditHabit/EditHabitController.swift
Normal file
87
ios-native/Application/EditHabit/EditHabitController.swift
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
43
ios-native/Application/Info.plist
Normal file
43
ios-native/Application/Info.plist
Normal file
@@ -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>
|
||||
7
ios-native/Application/Launch.storyboard
Normal file
7
ios-native/Application/Launch.storyboard
Normal file
@@ -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>
|
||||
41
ios-native/Application/ListHabits/ListHabitsController.swift
Normal file
41
ios-native/Application/ListHabits/ListHabitsController.swift
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
22
ios-native/Tests/Info.plist
Normal file
22
ios-native/Tests/Info.plist
Normal file
@@ -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 */; };
|
||||
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 = "<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>"; };
|
||||
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>"; };
|
||||
@@ -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 = "<group>";
|
||||
@@ -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 = "<group>";
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))")
|
||||
}
|
||||
|
||||
@@ -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 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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<NavigatorIOS
|
||||
translucent={false}
|
||||
initialRoute={{
|
||||
component: ListHabitsScene,
|
||||
title: 'Habits',
|
||||
rightButtonSystemIcon: 'add',
|
||||
}}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
);
|
||||
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 (
|
||||
<NavigatorIOS
|
||||
ref={(c) => { navigator = c; }}
|
||||
translucent={false}
|
||||
initialRoute={routes.index}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AppRegistry.registerComponent('LoopHabitTracker', () => RootComponent);
|
||||
|
||||
32
react-native/package-lock.json
generated
32
react-native/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
128
react-native/src/components/EditHabit/index.js
vendored
Normal file
128
react-native/src/components/EditHabit/index.js
vendored
Normal file
@@ -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({
|
||||
checkmarkBox: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
width: 55,
|
||||
height: 55,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
checkmark: {
|
||||
fontFamily: 'FontAwesome',
|
||||
fontSize: 14,
|
||||
fontSize: 17,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -17,12 +17,15 @@
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 (
|
||||
<FlatList
|
||||
style={styles.container}
|
||||
data={habits}
|
||||
renderItem={({ item }) => (
|
||||
<View style={styles.item}>
|
||||
<View style={styles.ringContainer}>
|
||||
<Ring
|
||||
color={Colors[item.color]}
|
||||
size={14}
|
||||
strokeWidth={20}
|
||||
percentage={Math.random()}
|
||||
/>
|
||||
<TouchableHighlight onPress={() => onClickHabit(item.key)}>
|
||||
<View style={styles.item}>
|
||||
<View style={styles.ringContainer}>
|
||||
<Ring
|
||||
color={Colors[item.color]}
|
||||
size={14}
|
||||
strokeWidth={20}
|
||||
percentage={Math.random()}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.labelContainer}>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={{
|
||||
fontSize: 17,
|
||||
color: Colors[item.color],
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity onPress={() => onClickCheckmark(item.key, 1)}>
|
||||
<CheckmarkButton color={Colors[item.color]} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => onClickCheckmark(item.key, 2)}>
|
||||
<CheckmarkButton color={Colors[item.color]} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => onClickCheckmark(item.key, 3)}>
|
||||
<CheckmarkButton color={Colors[item.color]} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.labelContainer}>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: Colors[item.color],
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
</View>
|
||||
<CheckmarkButton color={Colors[item.color]} />
|
||||
<CheckmarkButton color={Colors[item.color]} />
|
||||
<CheckmarkButton color={Colors[item.color]} />
|
||||
<CheckmarkButton color={Colors[item.color]} />
|
||||
</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({
|
||||
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);
|
||||
|
||||
26
react-native/src/components/ListHabits/index.js
vendored
26
react-native/src/components/ListHabits/index.js
vendored
@@ -17,6 +17,7 @@
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 (
|
||||
<View style={styles.container}>
|
||||
<HabitListHeader />
|
||||
<HabitList />
|
||||
</View>
|
||||
);
|
||||
export default class ListHabitsScene extends React.Component {
|
||||
render() {
|
||||
const { onClickHabit, onClickCheckmark } = this.props;
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<HabitListHeader />
|
||||
<HabitList
|
||||
onClickHabit={onClickHabit}
|
||||
onClickCheckmark={onClickCheckmark}
|
||||
/>
|
||||
</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/>.
|
||||
*/
|
||||
|
||||
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 (
|
||||
<Svg height={size} width={size} viewBox="0 0 100 100">
|
||||
<Circle cx={50} cy={50} r={50} fill={color} />
|
||||
<Circle cx={50} cy={50} r={30} fill={Colors.itemBackground} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
ColorCircle.propTypes = {
|
||||
size: PropTypes.number.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
};
|
||||
1
react-native/src/helpers/FontAwesome.js
vendored
1
react-native/src/helpers/FontAwesome.js
vendored
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
check: '\uf00c',
|
||||
times: '\uf00d',
|
||||
chevronRight: '\uf054',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user