diff --git a/app/src/main/java/com/dnsc/plaindo/MainActivity.kt b/app/src/main/java/com/dnsc/plaindo/MainActivity.kt index f56805a..da1acc8 100644 --- a/app/src/main/java/com/dnsc/plaindo/MainActivity.kt +++ b/app/src/main/java/com/dnsc/plaindo/MainActivity.kt @@ -10,10 +10,11 @@ import androidx.activity.result.contract.ActivityResultContracts import com.dnsc.plaindo.data.TodoFileRepository import com.dnsc.plaindo.ui.theme.PlaindoTheme +const val TAG = "MainActivity" + class MainActivity : ComponentActivity() { private lateinit var todoFileRepository: TodoFileRepository private lateinit var openDocumentLauncher: ActivityResultLauncher> - private val TAG = "MainActivity" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -36,7 +37,11 @@ class MainActivity : ComponentActivity() { if (uri != null) { // TODO: Store uri in shared preferences // TODO: Persist access to URI + todoFileRepository.todoFileUri = uri todoFileRepository.load(uri) + todoFileRepository.todos.forEach { todo -> + Log.d(TAG, todo.toString()) + } } else { // TODO: Handle user cancelled selection Log.d(TAG, "No document selected") diff --git a/app/src/main/java/com/dnsc/plaindo/data/Todo.kt b/app/src/main/java/com/dnsc/plaindo/data/Todo.kt new file mode 100644 index 0000000..3962765 --- /dev/null +++ b/app/src/main/java/com/dnsc/plaindo/data/Todo.kt @@ -0,0 +1,15 @@ +package com.dnsc.plaindo.data + +import java.time.LocalDate + +data class Todo( + val id: Number, + val description: String, + val completed: Boolean? = false, + val priority: Char? = null, + val completion: LocalDate? = null, + val creation: LocalDate? = null, + val projects: List? = null, + val contexts: List? = null, + val kvTags: MutableMap? = null +) diff --git a/app/src/main/java/com/dnsc/plaindo/data/TodoFileRepository.kt b/app/src/main/java/com/dnsc/plaindo/data/TodoFileRepository.kt index c1ea45f..8127a53 100644 --- a/app/src/main/java/com/dnsc/plaindo/data/TodoFileRepository.kt +++ b/app/src/main/java/com/dnsc/plaindo/data/TodoFileRepository.kt @@ -2,23 +2,38 @@ package com.dnsc.plaindo.data import android.content.Context import android.net.Uri -import android.util.Log import java.io.BufferedReader import java.io.InputStreamReader class TodoFileRepository(val context: Context) { + var todoFileUri: Uri? = null + val todos: ArrayList = ArrayList() + fun load(uri: Uri) { context.contentResolver.openInputStream(uri)?.use { inputStream -> BufferedReader(InputStreamReader(inputStream)).use { reader -> var line: String? = reader.readLine() - while (line != null) { - Log.d("Plaindo", line) + var currentLine = 0 - // TODO: Add task to data structure + while (line != null) { + val todoTxtParser = TodoTxtParser() + val todo = todoTxtParser.parse(currentLine, line) + todos.add(todo) line = reader.readLine() + currentLine++ } } } } + + fun refresh() { + if (todoFileUri == null) { + // TODO: Show file picker again + throw Exception("No todo file selected") + } + + // TODO: Handle more safely + load(todoFileUri!!) + } } \ No newline at end of file diff --git a/app/src/main/java/com/dnsc/plaindo/data/TodoTxtParser.kt b/app/src/main/java/com/dnsc/plaindo/data/TodoTxtParser.kt new file mode 100644 index 0000000..f52905f --- /dev/null +++ b/app/src/main/java/com/dnsc/plaindo/data/TodoTxtParser.kt @@ -0,0 +1,159 @@ +package com.dnsc.plaindo.data + +import android.util.Log +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import kotlin.text.isWhitespace + +class TodoTxtParser { + fun parse(id: Int, line: String): Todo { + val elements = line.split(" ") + + // First rough draft, easy to read + // Do not want to go for regexes if possible + // Should be more performant to use a single loop here + // Will implement an alternative solution if problems + // arise + val completed = getCompleted(elements) + val priority = getPriority(elements, completed) + val completion = getCompletion(elements, completed) + val creation = getCreation(elements, completed) + val projects = getProjects(elements) + val contexts = getContexts(elements) + val kvTags = getKvTags(elements) + val description = getDescription(elements) + + return Todo( + id = id, + description = description, + completed = completed, + priority = priority, + completion = completion, + creation = creation, + projects = projects, + contexts = contexts, + kvTags = kvTags + ) + } + + private fun getCompleted(elements: List): Boolean { + return elements[0].startsWith('x') + } + + private fun getPriority(elements: List, completed: Boolean): Char? { + val priorityIndex = if (completed) 1 else 0 + val hasPriority = isPriorityText(elements[priorityIndex]) + + return if (hasPriority) { + elements[priorityIndex][1] + } else { + null + } + } + + private fun getCompletion(elements: List, completed: Boolean): LocalDate? { + if (completed == false) { + return null + } + + val completionIndex = if (isPriorityText(elements[1])) 2 else 1 + val creationIndex = completionIndex + 1 + + Log.d("TodoTxtParser", "creation: ${elements[creationIndex]}") + Log.d("TodoTxtParser", "creation isDate: ${isDate(elements[creationIndex])}") + + return if (!isDate(elements[creationIndex])) { + null + } else { + parseDate(elements[completionIndex]) + } + + } + + private fun getCreation(elements: List, completed: Boolean): LocalDate? { + val baseIndex = if (completed) 1 else 0 + val completionIndex = if (isPriorityText(elements[0])) 2 + baseIndex else 1 + baseIndex + val creationIndex = completionIndex + 1 + + if (!isDate(elements[completionIndex])) { + return null + } + + return if (isDate(elements[creationIndex])) { + parseDate(elements[creationIndex]) + } else { + parseDate(elements[completionIndex]) + } + } + + private fun getProjects(elements: List): List { + return elements.filter { it.startsWith('+') } + } + + private fun getContexts(elements: List): List { + return elements.filter { it.startsWith('@') } + } + + private fun getKvTags(elements: List): MutableMap { + val map: MutableMap = mutableMapOf() + val kvTags = elements.filter { isKvTag(it) } + + kvTags.forEach { + map.put(it.split(":")[0], it.split(":")[1]) + } + + return map + } + + private fun getDescription(elements: List): String { + val filtered = elements.filter { + val isPriority = isPriorityText(it) + val isCompleted = it[0] == 'x' + val isProject = it.startsWith('+') + val isContext = it.startsWith('@') + val isDate = isDate(it) + val isKvTag = it.contains(":") + + return@filter !(isPriority || isCompleted || isProject || isContext || isDate || isKvTag) + } + + return filtered.joinToString(" ") + } + + private fun isPriorityText(element: String): Boolean { + return element.length == 3 && element.startsWith('(') && element.endsWith(')') + } + + private fun isKvTag(element: String): Boolean { + val separatorIndex = element.indexOf(':') + val beforeSeparatorIndex = separatorIndex - 1 + val afterSeparatorIndex = separatorIndex + 1 + + if (beforeSeparatorIndex < 0 || afterSeparatorIndex >= element.length) { + return false + } + + if ( + element[beforeSeparatorIndex].isWhitespace() || element[afterSeparatorIndex].isWhitespace() + ) { + return false + } else { + return true + } + } + + private fun isDate(element: String): Boolean { + try { + parseDate(element) + return true + } catch (e: Exception) { + return false + } + } + + // TODO: Extend for date time + private fun parseDate(dateString: String): LocalDate { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + return LocalDate.parse(dateString, formatter) + } +} \ No newline at end of file