Files
Vellum/app/src/main/java/space/bloggin/vellum/UserData.kt
T

152 lines
6.9 KiB
Kotlin

package space.bloggin.vellum
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import androidx.compose.runtime.mutableIntStateOf
import java.time.LocalDate
data class Note(val ref: Ref, val text: String, val updated: Long)
data class Mark(val ref: Ref, val created: Long)
/** Bookmarks, highlights, notes and reading-plan progress. Everything stays on the phone. */
class UserData private constructor(ctx: Context) : SQLiteOpenHelper(ctx, "user.db", null, 1) {
/** bumps whenever anything changes, so the UI can recompose */
val rev = mutableIntStateOf(0)
private val highlights = HashMap<Int, Int>()
private val notes = HashMap<Int, Note>()
private val bookmarks = HashMap<Int, Long>()
override fun onCreate(db: SQLiteDatabase) {
db.execSQL("CREATE TABLE highlight(k INTEGER PRIMARY KEY, color INTEGER, created INTEGER)")
db.execSQL("CREATE TABLE note(k INTEGER PRIMARY KEY, text TEXT, updated INTEGER)")
db.execSQL("CREATE TABLE bookmark(k INTEGER PRIMARY KEY, created INTEGER)")
db.execSQL("CREATE TABLE plan(id TEXT PRIMARY KEY, started TEXT)")
db.execSQL("CREATE TABLE plan_day(plan TEXT, day INTEGER, done TEXT, PRIMARY KEY(plan,day))")
db.execSQL("CREATE TABLE activity(date TEXT PRIMARY KEY)")
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {}
init {
val db = readableDatabase
db.rawQuery("SELECT k,color FROM highlight", null).use { while (it.moveToNext()) highlights[it.getInt(0)] = it.getInt(1) }
db.rawQuery("SELECT k,text,updated FROM note", null).use { while (it.moveToNext()) notes[it.getInt(0)] = Note(Ref.fromKey(it.getInt(0)), it.getString(1), it.getLong(2)) }
db.rawQuery("SELECT k,created FROM bookmark", null).use { while (it.moveToNext()) bookmarks[it.getInt(0)] = it.getLong(1) }
}
private fun bump() { rev.intValue++ }
// highlights: color index 1..6, 0 = none
fun highlight(r: Ref): Int = highlights[r.key()] ?: 0
fun setHighlight(refs: Collection<Ref>, color: Int) {
val db = writableDatabase
db.beginTransaction()
try {
for (r in refs) {
if (color == 0) { db.delete("highlight", "k=?", arrayOf(r.key().toString())); highlights.remove(r.key()) }
else {
db.insertWithOnConflict("highlight", null, ContentValues().apply {
put("k", r.key()); put("color", color); put("created", System.currentTimeMillis())
}, SQLiteDatabase.CONFLICT_REPLACE)
highlights[r.key()] = color
}
}
db.setTransactionSuccessful()
} finally { db.endTransaction() }
bump()
}
fun allHighlights(): List<Pair<Ref, Int>> = highlights.entries.sortedBy { it.key }.map { Ref.fromKey(it.key) to it.value }
fun note(r: Ref): Note? = notes[r.key()]
fun setNote(r: Ref, text: String) {
val db = writableDatabase
if (text.isBlank()) { db.delete("note", "k=?", arrayOf(r.key().toString())); notes.remove(r.key()) }
else {
val now = System.currentTimeMillis()
db.insertWithOnConflict("note", null, ContentValues().apply { put("k", r.key()); put("text", text); put("updated", now) }, SQLiteDatabase.CONFLICT_REPLACE)
notes[r.key()] = Note(r, text, now)
}
bump()
}
fun allNotes(): List<Note> = notes.values.sortedByDescending { it.updated }
fun isBookmarked(r: Ref) = bookmarks.containsKey(r.key())
fun toggleBookmark(r: Ref) {
val db = writableDatabase
if (isBookmarked(r)) { db.delete("bookmark", "k=?", arrayOf(r.key().toString())); bookmarks.remove(r.key()) }
else {
val now = System.currentTimeMillis()
db.insertWithOnConflict("bookmark", null, ContentValues().apply { put("k", r.key()); put("created", now) }, SQLiteDatabase.CONFLICT_REPLACE)
bookmarks[r.key()] = now
}
bump()
}
fun allBookmarks(): List<Mark> = bookmarks.entries.sortedByDescending { it.value }.map { Mark(Ref.fromKey(it.key), it.value) }
// ---------------- reading plans ----------------
fun planStarted(id: String): LocalDate? =
readableDatabase.rawQuery("SELECT started FROM plan WHERE id=?", arrayOf(id)).use { if (it.moveToFirst()) LocalDate.parse(it.getString(0)) else null }
fun startPlan(id: String) {
val db = writableDatabase
db.delete("plan_day", "plan=?", arrayOf(id))
db.insertWithOnConflict("plan", null, ContentValues().apply { put("id", id); put("started", LocalDate.now().toString()) }, SQLiteDatabase.CONFLICT_REPLACE)
bump()
}
fun stopPlan(id: String) {
writableDatabase.delete("plan", "id=?", arrayOf(id))
writableDatabase.delete("plan_day", "plan=?", arrayOf(id))
bump()
}
fun daysDone(id: String): Set<Int> =
readableDatabase.rawQuery("SELECT day FROM plan_day WHERE plan=?", arrayOf(id)).use { c ->
buildSet { while (c.moveToNext()) add(c.getInt(0)) }
}
fun setDayDone(id: String, day: Int, done: Boolean) {
val db = writableDatabase
if (done) {
db.insertWithOnConflict("plan_day", null, ContentValues().apply { put("plan", id); put("day", day); put("done", LocalDate.now().toString()) }, SQLiteDatabase.CONFLICT_REPLACE)
db.insertWithOnConflict("activity", null, ContentValues().apply { put("date", LocalDate.now().toString()) }, SQLiteDatabase.CONFLICT_IGNORE)
} else db.delete("plan_day", "plan=? AND day=?", arrayOf(id, day.toString()))
bump()
}
/** Consecutive days (ending today, or yesterday if today isn't done yet) with a finished plan reading. */
fun streak(): Int {
val dates = readableDatabase.rawQuery("SELECT date FROM activity", null).use { c ->
buildSet { while (c.moveToNext()) add(c.getString(0)) }
}
var d = LocalDate.now()
if (!dates.contains(d.toString())) d = d.minusDays(1)
var n = 0
while (dates.contains(d.toString())) { n++; d = d.minusDays(1) }
return n
}
fun bestStreak(): Int {
val dates = readableDatabase.rawQuery("SELECT date FROM activity ORDER BY date", null).use { c ->
buildList { while (c.moveToNext()) add(LocalDate.parse(c.getString(0))) }
}
var best = 0; var cur = 0; var prev: LocalDate? = null
for (d in dates) {
cur = if (prev != null && prev.plusDays(1) == d) cur + 1 else 1
best = maxOf(best, cur); prev = d
}
return best
}
companion object {
@Volatile private var instance: UserData? = null
fun get(ctx: Context): UserData = instance ?: synchronized(this) {
instance ?: UserData(ctx.applicationContext).also { instance = it }
}
}
}