Implements first version of TodoTxtParse

This commit is contained in:
Dennis Schoepf 2025-04-12 00:40:53 +02:00
parent 04165e0068
commit fb9983c6df
4 changed files with 199 additions and 5 deletions

View file

@ -10,10 +10,11 @@ import androidx.activity.result.contract.ActivityResultContracts
import com.dnsc.plaindo.data.TodoFileRepository import com.dnsc.plaindo.data.TodoFileRepository
import com.dnsc.plaindo.ui.theme.PlaindoTheme import com.dnsc.plaindo.ui.theme.PlaindoTheme
const val TAG = "MainActivity"
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private lateinit var todoFileRepository: TodoFileRepository private lateinit var todoFileRepository: TodoFileRepository
private lateinit var openDocumentLauncher: ActivityResultLauncher<Array<String>> private lateinit var openDocumentLauncher: ActivityResultLauncher<Array<String>>
private val TAG = "MainActivity"
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -36,7 +37,11 @@ class MainActivity : ComponentActivity() {
if (uri != null) { if (uri != null) {
// TODO: Store uri in shared preferences // TODO: Store uri in shared preferences
// TODO: Persist access to URI // TODO: Persist access to URI
todoFileRepository.todoFileUri = uri
todoFileRepository.load(uri) todoFileRepository.load(uri)
todoFileRepository.todos.forEach { todo ->
Log.d(TAG, todo.toString())
}
} else { } else {
// TODO: Handle user cancelled selection // TODO: Handle user cancelled selection
Log.d(TAG, "No document selected") Log.d(TAG, "No document selected")

View file

@ -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<String>? = null,
val contexts: List<String>? = null,
val kvTags: MutableMap<String, String>? = null
)

View file

@ -2,23 +2,38 @@ package com.dnsc.plaindo.data
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.Log
import java.io.BufferedReader import java.io.BufferedReader
import java.io.InputStreamReader import java.io.InputStreamReader
class TodoFileRepository(val context: Context) { class TodoFileRepository(val context: Context) {
var todoFileUri: Uri? = null
val todos: ArrayList<Todo> = ArrayList()
fun load(uri: Uri) { fun load(uri: Uri) {
context.contentResolver.openInputStream(uri)?.use { inputStream -> context.contentResolver.openInputStream(uri)?.use { inputStream ->
BufferedReader(InputStreamReader(inputStream)).use { reader -> BufferedReader(InputStreamReader(inputStream)).use { reader ->
var line: String? = reader.readLine() var line: String? = reader.readLine()
while (line != null) { var currentLine = 0
Log.d("Plaindo", line)
// TODO: Add task to data structure while (line != null) {
val todoTxtParser = TodoTxtParser()
val todo = todoTxtParser.parse(currentLine, line)
todos.add(todo)
line = reader.readLine() 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!!)
}
} }

View file

@ -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<String>): Boolean {
return elements[0].startsWith('x')
}
private fun getPriority(elements: List<String>, 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<String>, 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<String>, 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<String>): List<String> {
return elements.filter { it.startsWith('+') }
}
private fun getContexts(elements: List<String>): List<String> {
return elements.filter { it.startsWith('@') }
}
private fun getKvTags(elements: List<String>): MutableMap<String, String> {
val map: MutableMap<String, String> = 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>): 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)
}
}