package com.vgmlr.kerf
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import org.json.JSONObject
class NoteViewModel(application: Application) : AndroidViewModel(application) {
private val dao = AppDb.getDatabase(application).dao()
private var saveJob: Job? = null
private val _sharedText = MutableStateFlow<String?>(null)
val sharedText: StateFlow<String?> = _sharedText.asStateFlow()
private val _sharedFile = MutableStateFlow<Pair<String, String>?>(null)
val sharedFile: StateFlow<Pair<String, String>?> = _sharedFile.asStateFlow()
private val _kerfImport = MutableStateFlow<Pair<String, String>?>(null)
val kerfImport: StateFlow<Pair<String, String>?> = _kerfImport.asStateFlow()
val allNotes: StateFlow<List<Note>> = dao.getAll()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
fun observeNote(id: Int): Flow<Note?> = dao.getByIdFlow(id)
fun setSharedText(text: String?) { _sharedText.value = text }
fun setKerfImport(data: Pair<String, String>?) { _kerfImport.value = data }
fun setSharedFile(file: Pair<String, String>?) {
if (file == null) {
_sharedFile.value = null
return
}
viewModelScope.launch {
val currentNotes = allNotes.value
var finalTitle = file.first
var count = 1
while (currentNotes.any { it.title.equals(finalTitle, ignoreCase = true) }) {
count++
finalTitle = "${file.first} ($count)"
}
_sharedFile.value = finalTitle to file.second
}
}
fun toggleReplyMode(id: Int) {
viewModelScope.launch {
val current = dao.getById(id) ?: return@launch
if (current.replies == null) {
dao.update(current.copy(replies = JSONObject().toString()))
} else {
dao.update(current.copy(replies = null))
}
}
}
fun updateReply(id: Int, itemNum: Int, text: String) {
viewModelScope.launch {
val current = dao.getById(id) ?: return@launch
val json = if (current.replies != null) JSONObject(current.replies) else JSONObject()
json.put(itemNum.toString(), text)
dao.update(current.copy(replies = json.toString(), updatedAt = System.currentTimeMillis()))
}
}
fun deleteLineWithReplySync(id: Int, title: String, content: String, lineIndex: Int) {
saveJob?.cancel()
viewModelScope.launch {
val note = dao.getById(id) ?: return@launch
val json = if (note.replies != null) JSONObject(note.replies) else null
val newContent = KerfUtils.deleteAndReindex(content, lineIndex)
val newJson = if (json != null) {
val updated = JSONObject()
val lines = content.lines()
val deletedItemMatch = KerfUtils.ItemRegex.find(lines[lineIndex].trim())
val deletedNum = deletedItemMatch?.groupValues?.get(1)?.toIntOrNull() ?: -1
val it = json.keys()
while (it.hasNext()) {
val keyStr = it.next()
val key = keyStr.toInt()
val value = json.getString(keyStr)
if (key < deletedNum) {
updated.put(key.toString(), value)
} else if (key > deletedNum) {
updated.put((key - 1).toString(), value)
}
}
updated.toString()
} else null
dao.update(note.copy(title = title, content = newContent, replies = newJson, updatedAt = System.currentTimeMillis()))
}
}
fun addNote(isImported: Boolean = false, onDone: (Int) -> Unit) {
viewModelScope.launch {
val now = System.currentTimeMillis()
val id = dao.insert(Note(title = "", content = KerfUtils.DEFAULT_PREFIX, isImported = isImported, createdAt = now, updatedAt = now))
onDone(id.toInt())
}
}
fun updateNote(id: Int, title: String, content: String) {
saveJob?.cancel()
saveJob = viewModelScope.launch { delay(2000); performSave(id, title, content) }
}
fun updateNoteImmediate(id: Int, title: String, content: String, isImported: Boolean? = null) {
saveJob?.cancel()
viewModelScope.launch { performSave(id, title, content, isImported) }
}
private suspend fun performSave(id: Int, title: String, content: String, isImported: Boolean? = null) {
val current = dao.getById(id)
if (current != null) {
val nextImported = isImported ?: current.isImported
if (current.title != title || current.content != content || current.isImported != nextImported) {
dao.update(current.copy(title = title, content = content, isImported = nextImported, updatedAt = System.currentTimeMillis()))
}
}
}
fun togglePin(id: Int) {
viewModelScope.launch {
dao.getById(id)?.let {
dao.update(it.copy(isPinned = !it.isPinned))
}
}
}
fun updateLastShared(id: Int) {
viewModelScope.launch { dao.getById(id)?.let { dao.update(it.copy(lastShared = System.currentTimeMillis())) } }
}
fun updateLastCopied(id: Int) {
viewModelScope.launch { dao.getById(id)?.let { dao.update(it.copy(lastCopied = System.currentTimeMillis())) } }
}
fun clearNoteAndSharedDate(id: Int) {
saveJob?.cancel()
viewModelScope.launch {
dao.getById(id)?.let { current ->
dao.update(current.copy(content = KerfUtils.DEFAULT_PREFIX, replies = null, lastShared = null, lastCopied = null, updatedAt = System.currentTimeMillis()))
}
}
}
fun deleteNote(id: Int) = viewModelScope.launch { dao.deleteById(id) }
fun appendContent(id: Int, text: String) {
viewModelScope.launch {
val note = dao.getById(id) ?: return@launch
val lines = note.content.lines()
var lastNum = 0
lines.forEach { line ->
KerfUtils.ItemRegex.find(line.trim())?.let { match ->
val num = match.groupValues[1].toIntOrNull() ?: 0
if (num > lastNum) lastNum = num
}
}
val prefix = KerfUtils.DEFAULT_PREFIX
val isInitial = note.content.isBlank() || note.content.trim() == prefix.trim()
val newContent = if (isInitial) {
"$prefix$text"
} else {
val cleanBase = note.content.trimEnd()
"$cleanBase\n\n${lastNum + 1}. $text"
}
dao.update(note.copy(content = newContent, updatedAt = System.currentTimeMillis()))
}
}
}