Pith - kerf
kerf/app/src/main/java/com/vgmlr/kerf/KerfUtils.kt [5.4 kb]
Modified: 23:09:02 55 026 (13 May 026)
17 Days Ago
package com.vgmlr.kerf

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.OpenableColumns
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit

object KerfUtils {
    val UrlRegex = Regex("""(https?://\S+)""")
    val ItemRegex = Regex("""^(\d+)\.""")
    const val DEFAULT_PREFIX = "1. "

    fun parseKerfText(text: String): Pair<String, String>? {
        if (!text.startsWith("Kerf: ")) return null
        val doubleNewlineIndex = text.indexOf("\n\n")
        val title = if (doubleNewlineIndex != -1) {
            text.substring(6, doubleNewlineIndex).trim()
        } else {
            text.substring(6).trim()
        }
        val content = if (doubleNewlineIndex != -1) {
            text.substring(doubleNewlineIndex + 2).ifBlank { DEFAULT_PREFIX }
        } else {
            DEFAULT_PREFIX
        }
        return title to content
    }

    fun countNumberedItems(content: String): Int {
        return content.lines().count { line ->
            ItemRegex.find(line.trim()) != null
        }
    }

    fun deleteAndReindex(content: String, lineIndex: Int): String {
        val lines = content.lines().toMutableList()
        if (lineIndex < 0 || lineIndex >= lines.size) return content

        val wasNumbered = ItemRegex.find(lines[lineIndex].trim()) != null
        lines.removeAt(lineIndex)

        if (lineIndex < lines.size && wasNumbered) {
            val nextLine = lines[lineIndex]
            val isNextNumbered = ItemRegex.find(nextLine.trim()) != null
            if (nextLine.isNotBlank() && !isNextNumbered) {
                lines.removeAt(lineIndex)
            }
        }

        if (lineIndex < lines.size && lines[lineIndex].isBlank()) {
            lines.removeAt(lineIndex)
        }

        var currentNum = 1
        return lines.joinToString("\n") { line ->
            val match = ItemRegex.find(line.trim())
            if (match != null) {
                val cleanLine = line.trim().replaceFirst(ItemRegex, "").trim()
                "$currentNum. $cleanLine".also { currentNum++ }
            } else {
                line
            }
        }
    }

    fun cleanupCache(context: Context) {
        try {
            context.cacheDir.listFiles { _, name -> name.endsWith(".kerf") }?.forEach { it.delete() }
        } catch (_: Exception) {}
    }

    fun shareReplies(context: Context, title: String, repliesRaw: String?) {
        if (repliesRaw == null) return
        val json = JSONObject(repliesRaw)
        val keys = mutableListOf<Int>()
        val it = json.keys()
        while (it.hasNext()) {
            val key = it.next().toIntOrNull()
            if (key != null) keys.add(key)
        }
        keys.sort()

        val list = keys.joinToString("\n\n") { key ->
            val text = json.getString(key.toString())
            if (text.startsWith("$key. ")) text else "$key. $text"
        }

        if (list.isNotBlank()) {
            val finalContent = if (title.isNotBlank()) "Kerf: Reply: $title\n\n$list" else list
            shareNote(context, finalContent)
        }
    }

    fun importNoteFromUri(context: Context, uri: Uri): Pair<String, String>? {
        return try {
            val contentResolver = context.contentResolver
            val fileName = contentResolver.query(uri, null, null, null, null)?.use { cursor ->
                val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
                if (nameIndex != -1 && cursor.moveToFirst()) {
                    cursor.getString(nameIndex)
                } else null
            } ?: "Imported List"

            val title = fileName.removeSuffix(".kerf").replace("_", " ")
            val content = contentResolver.openInputStream(uri)?.bufferedReader()?.use { it.readText() }

            if (content != null) title to content else null
        } catch (_: Exception) {
            null
        }
    }

    fun formatLastSharedDate(timestamp: Long?): String {
        if (timestamp == null || timestamp == 0L) return "n/a"
        val cal = Calendar.getInstance().apply { timeInMillis = timestamp }
        val gYear = cal.get(Calendar.YEAR)
        val isBeforeMarch20 = (cal.get(Calendar.MONTH) < Calendar.MARCH) ||
                (cal.get(Calendar.MONTH) == Calendar.MARCH && cal.get(Calendar.DAY_OF_MONTH) < 20)
        val cycleStartYear = if (isBeforeMarch20) gYear - 1 else gYear
        val altYear = cycleStartYear % 100
        val cycleStart = Calendar.getInstance().apply {
            set(Calendar.YEAR, cycleStartYear); set(Calendar.MONTH, Calendar.MARCH)
            set(Calendar.DAY_OF_MONTH, 20); set(Calendar.HOUR_OF_DAY, 0)
            set(Calendar.MINUTE, 0); set(Calendar.SECOND, 0); set(Calendar.MILLISECOND, 0)
        }
        val diff = timestamp - cycleStart.timeInMillis
        val altDayOfYear = TimeUnit.MILLISECONDS.toDays(diff) + 1
        val time = SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date(timestamp))
        val gregDate = SimpleDateFormat("dd MMM yy", Locale.getDefault()).format(Date(timestamp))
        return "$time $altDayOfYear 0$altYear ($gregDate)"
    }
}

fun shareNote(context: Context, content: String) {
    val shareIntent = Intent().apply {
        action = Intent.ACTION_SEND
        type = "text/plain"
        putExtra(Intent.EXTRA_TEXT, content)
    }
    context.startActivity(Intent.createChooser(shareIntent, "Share List"))
}
Updates
Shim - Android 70.026.1
Wedge - Linux 68.026.1
Wedge - Android 68.026.1
Taper - Linux 64.026.1
Ayh Extension - Chrome 63.026.1
Dev
TVShow (227) 'CSA'
TVShow (228) 'APT'
TVProgram (83) 'BXT'
Miter Update(s)
Shim (Dictation)

Menu
Calendar
Project Tin (024/029)
Miter
RSS Feed
User Avatar
@vgmlr
=SUM(parts)