Pith - kerf
kerf/app/src/main/java/com/vgmlr/kerf/KerfUtils.kt [10.4 kb]
Modified: 23:16:31 89 026 (16 Jun 026)
7 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
import androidx.core.net.toUri

object KerfUtils {
    val UrlRegex = Regex("""(https?://\S+|content://\S+|file://\S+|/storage/\S+|/sdcard/\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()
        if (lineIndex < 0 || lineIndex >= lines.size) return content

        val isDeletingNumberedItem = ItemRegex.find(lines[lineIndex].trim()) != null
        
        if (!isDeletingNumberedItem) {
            val mutableLines = lines.toMutableList()
            mutableLines.removeAt(lineIndex)
            var currentNum = 1
            return mutableLines.joinToString("\n") { line ->
                if (ItemRegex.find(line.trim()) != null) {
                    val cleanLine = line.trim().replaceFirst(ItemRegex, "").trim()
                    "$currentNum. $cleanLine".also { currentNum++ }
                } else line
            }
        }
        
        val parsedBlocks = mutableListOf<MutableList<String>>()
        var currentBlock = mutableListOf<String>()
        var targetBlockIndex = -1

        for (i in lines.indices) {
            val line = lines[i]
            if (ItemRegex.find(line.trim()) != null) {
                if (currentBlock.isNotEmpty()) parsedBlocks.add(currentBlock)
                currentBlock = mutableListOf(line)
                if (i == lineIndex) targetBlockIndex = parsedBlocks.size
            } else {
                currentBlock.add(line)
            }
        }
        if (currentBlock.isNotEmpty()) parsedBlocks.add(currentBlock)
        
        if (targetBlockIndex != -1) {
            parsedBlocks.removeAt(targetBlockIndex)
        }
        
        var currentNum = 1
        val newContent = StringBuilder()

        for (i in parsedBlocks.indices) {
            val block = parsedBlocks[i]
            val titleLine = block.first().trim()
            val cleanTitle = titleLine.replaceFirst(ItemRegex, "").trim()
            newContent.append("$currentNum. $cleanTitle\n")
            currentNum++
            
            var lastNonEmpty = block.size - 1
            while (lastNonEmpty > 0 && block[lastNonEmpty].trim().isEmpty()) {
                lastNonEmpty--
            }

            val hasTextComment = lastNonEmpty > 0
            
            for (j in 1..lastNonEmpty) {
                newContent.append(block[j]).append("\n")
            }
            
            if (i < parsedBlocks.size - 1) {
                if (hasTextComment) {
                    newContent.append("\n") 
                } else {
                    newContent.append("\n\n") 
                }
            }
        }

        val finalResult = newContent.toString()
        if (finalResult.isEmpty()) return DEFAULT_PREFIX + "\n"
        return finalResult
    }

    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", Locale.getDefault()).format(Date(timestamp))
        return "$time $altDayOfYear 0$altYear ($gregDate)"
    }
    fun getLocalFilePath(context: Context, uri: Uri): String? {
        if (uri.scheme == "file") {
            return uri.path
        } else if (uri.scheme == "content") {
            try {
                context.contentResolver.query(uri, arrayOf("_data"), null, null, null)?.use { cursor ->
                    if (cursor.moveToFirst()) {
                        val columnIndex = cursor.getColumnIndex("_data")
                        if (columnIndex != -1) {
                            return cursor.getString(columnIndex)
                        }
                    }
                }
            } catch (_: Exception) {}
        }
        return null
    }

    fun isKerfFile(context: Context, uri: Uri): Boolean {
        if (uri.path?.endsWith(".kerf", ignoreCase = true) == true) return true
        try {
            context.contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursor ->
                if (cursor.moveToFirst()) {
                    val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
                    if (nameIndex != -1) {
                        val fileName = cursor.getString(nameIndex)
                        if (fileName != null && fileName.endsWith(".kerf", ignoreCase = true)) {
                            return true
                        }
                    }
                }
            }
        } catch (_: Exception) {}
        return false
    }

    fun openAddress(context: Context, address: String) {
        try {
            if (address.startsWith("http://") || address.startsWith("https://")) {
                val intent = Intent(Intent.ACTION_VIEW, address.toUri()).apply {
                    addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                }
                context.startActivity(intent)
            } else {
                val uri = if (address.startsWith("content://") || address.startsWith("file://")) {
                    address.toUri()
                } else if (address.startsWith("/")) {
                    Uri.fromFile(java.io.File(address))
                } else {
                    address.toUri()
                }

                val intent = Intent(Intent.ACTION_VIEW).apply {
                    val mimeType = context.contentResolver.getType(uri) ?: run {
                        val path = uri.path ?: address
                        val ext = path.substringAfterLast('.', "").lowercase()
                        android.webkit.MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)
                    }
                    setDataAndType(uri, mimeType)
                    addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                    addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                }

                val oldPolicy = android.os.StrictMode.getVmPolicy()
                try {
                    android.os.StrictMode.setVmPolicy(android.os.StrictMode.VmPolicy.Builder().build())
                    context.startActivity(intent)
                } catch (_: Exception) {
                    val fallbackIntent = Intent(Intent.ACTION_VIEW, uri).apply {
                        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                    }
                    context.startActivity(fallbackIntent)
                } finally {
                    android.os.StrictMode.setVmPolicy(oldPolicy)
                }
            }
        } catch (_: Exception) {}
    }
}

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
OTC Applet - Linux 93.026.1
Wedge - Linux 90.026.1
Wedge - Android 90.026.1
Shim - Android 86.026.1
Kerf - Android 86.026.4
Dev
TVShow (227) 'CSA'
TVShow (228) 'APT'
TVProgram (83) 'BXT'
Miter Update(s)
Peen (Messaging)

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