Add app/src/main/java/space/bloggin/vellum
This commit is contained in:
@@ -0,0 +1,182 @@
|
|||||||
|
package space.bloggin.vellum
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Slider
|
||||||
|
import androidx.compose.material3.SliderDefaults
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.SwitchDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
val LocalBackdrop = compositionLocalOf { Backdrops.all[0] }
|
||||||
|
|
||||||
|
/** UI text (menus, buttons) uses the system sans; reading text uses the chosen serif. */
|
||||||
|
val UiFont = FontFamily.SansSerif
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BackdropFill(bd: Backdrop, modifier: Modifier = Modifier) {
|
||||||
|
if (bd.image != null) {
|
||||||
|
Image(painterResource(bd.image), null, modifier.fillMaxSize(), contentScale = ContentScale.Crop)
|
||||||
|
} else {
|
||||||
|
Box(modifier.fillMaxSize().background(Brush.linearGradient(bd.gradient!!)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Panel(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) {
|
||||||
|
val bd = LocalBackdrop.current
|
||||||
|
Column(
|
||||||
|
modifier
|
||||||
|
.background(bd.panel, RectangleShape)
|
||||||
|
.border(BorderStroke(1.dp, bd.faint.copy(alpha = 0.45f)), RectangleShape),
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Square button, the house style. */
|
||||||
|
@Composable
|
||||||
|
fun SqButton(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
icon: ImageVector? = null,
|
||||||
|
filled: Boolean = false,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
val bd = LocalBackdrop.current
|
||||||
|
val fg = if (filled) (if (bd.dark) Color(0xFF1A120A) else Color(0xFFF7EEDC)) else bd.ink
|
||||||
|
val bg = if (filled) (if (bd.dark) bd.ink else bd.ink) else Color.Transparent
|
||||||
|
Row(
|
||||||
|
modifier
|
||||||
|
.clip(RectangleShape)
|
||||||
|
.background(bg)
|
||||||
|
.border(1.dp, bd.ink.copy(alpha = if (enabled) 0.8f else 0.3f), RectangleShape)
|
||||||
|
.clickable(enabled = enabled, onClick = onClick)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 9.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
if (icon != null) {
|
||||||
|
Icon(icon, null, tint = fg.copy(alpha = if (enabled) 1f else 0.4f), modifier = Modifier.size(18.dp))
|
||||||
|
if (text.isNotEmpty()) Spacer(Modifier.width(6.dp))
|
||||||
|
}
|
||||||
|
if (text.isNotEmpty()) Text(text, color = fg.copy(alpha = if (enabled) 1f else 0.4f), fontFamily = UiFont, fontSize = 14.sp, maxLines = 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Icon with a small label underneath, used in the selection toolbar. */
|
||||||
|
@Composable
|
||||||
|
fun ToolButton(text: String, icon: ImageVector, active: Boolean = false, onClick: () -> Unit) {
|
||||||
|
val bd = LocalBackdrop.current
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.width(68.dp)
|
||||||
|
.background(if (active) bd.ink.copy(alpha = 0.12f) else Color.Transparent)
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Icon(icon, text, tint = bd.ink, modifier = Modifier.size(22.dp))
|
||||||
|
Spacer(Modifier.height(3.dp))
|
||||||
|
Text(text, color = bd.ink, fontSize = 11.sp, fontFamily = UiFont, maxLines = 1, overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Chip(text: String, selected: Boolean, modifier: Modifier = Modifier, family: FontFamily = UiFont, onClick: () -> Unit) {
|
||||||
|
val bd = LocalBackdrop.current
|
||||||
|
Box(
|
||||||
|
modifier
|
||||||
|
.background(if (selected) bd.ink else Color.Transparent, RectangleShape)
|
||||||
|
.border(1.dp, bd.ink.copy(alpha = 0.7f), RectangleShape)
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(text, color = if (selected) (if (bd.dark) Color(0xFF1A120A) else Color(0xFFF7EEDC)) else bd.ink, fontFamily = family, fontSize = 14.sp, maxLines = 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SectionLabel(text: String, modifier: Modifier = Modifier) {
|
||||||
|
val bd = LocalBackdrop.current
|
||||||
|
Text(text.uppercase(), modifier.padding(top = 18.dp, bottom = 8.dp), color = bd.rubric, fontFamily = Fonts.display, fontSize = 15.sp, letterSpacing = 1.5.sp)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ToggleRow(label: String, checked: Boolean, sub: String? = null, onChange: (Boolean) -> Unit) {
|
||||||
|
val bd = LocalBackdrop.current
|
||||||
|
Row(
|
||||||
|
Modifier.fillMaxWidth().clickable { onChange(!checked) }.padding(vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Text(label, color = bd.ink, fontFamily = UiFont, fontSize = 16.sp)
|
||||||
|
if (sub != null) Text(sub, color = bd.faint, fontFamily = UiFont, fontSize = 13.sp)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = checked, onCheckedChange = onChange,
|
||||||
|
colors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = if (bd.dark) Color(0xFF1A120A) else Color(0xFFF7EEDC),
|
||||||
|
checkedTrackColor = bd.ink,
|
||||||
|
uncheckedThumbColor = bd.ink,
|
||||||
|
uncheckedTrackColor = Color.Transparent,
|
||||||
|
uncheckedBorderColor = bd.ink,
|
||||||
|
checkedBorderColor = bd.ink,
|
||||||
|
),
|
||||||
|
thumbContent = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SliderRow(label: String, value: Float, range: ClosedFloatingPointRange<Float>, display: String, steps: Int = 0, onChange: (Float) -> Unit) {
|
||||||
|
val bd = LocalBackdrop.current
|
||||||
|
Column(Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
|
||||||
|
Row {
|
||||||
|
Text(label, color = bd.ink, fontFamily = UiFont, fontSize = 16.sp, modifier = Modifier.weight(1f))
|
||||||
|
Text(display, color = bd.faint, fontFamily = UiFont, fontSize = 14.sp)
|
||||||
|
}
|
||||||
|
Slider(
|
||||||
|
value = value, onValueChange = onChange, valueRange = range, steps = steps,
|
||||||
|
colors = SliderDefaults.colors(thumbColor = bd.ink, activeTrackColor = bd.ink, inactiveTrackColor = bd.ink.copy(alpha = 0.25f),
|
||||||
|
activeTickColor = Color.Transparent, inactiveTickColor = Color.Transparent)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Rule(modifier: Modifier = Modifier, alpha: Float = 0.35f, thickness: Dp = 1.dp) {
|
||||||
|
val bd = LocalBackdrop.current
|
||||||
|
Box(modifier.fillMaxWidth().height(thickness).background(bd.faint.copy(alpha = alpha)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun inkOn(bd: Backdrop) = if (bd.dark) Color(0xFF1A120A) else Color(0xFFF7EEDC)
|
||||||
|
|
||||||
|
val RoundSmall = RoundedCornerShape(0.dp)
|
||||||
|
|
||||||
|
fun uiStyle(bd: Backdrop, size: Int = 15) = TextStyle(color = bd.ink, fontFamily = UiFont, fontSize = size.sp)
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package space.bloggin.vellum
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.AlarmManager
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.appwidget.AppWidgetProvider
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.widget.RemoteViews
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
|
object Today {
|
||||||
|
data class V(val ref: Ref, val label: String, val text: String)
|
||||||
|
|
||||||
|
fun get(ctx: Context): V {
|
||||||
|
val bible = Bible.get(ctx)
|
||||||
|
val s = Settings.get(ctx)
|
||||||
|
val r = bible.parseRef(Votd.refFor()) ?: Ref(43, 3, 16)
|
||||||
|
val ref = if (r.verse == 0) r.copy(verse = 1) else r
|
||||||
|
return V(ref, bible.refLabel(ref) + " " + bible.trCode(s.tr), bible.plainRange(s.tr, ref))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openIntent(ctx: Context, ref: Ref, code: Int): PendingIntent {
|
||||||
|
val i = Intent(ctx, MainActivity::class.java).apply {
|
||||||
|
action = "space.bloggin.vellum.OPEN_${ref.key()}"
|
||||||
|
putExtra("ref", ref.key())
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
|
}
|
||||||
|
return PendingIntent.getActivity(ctx, code, i, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VotdWidget : AppWidgetProvider() {
|
||||||
|
override fun onUpdate(ctx: Context, mgr: AppWidgetManager, ids: IntArray) {
|
||||||
|
update(ctx, mgr, ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun update(ctx: Context, mgr: AppWidgetManager = AppWidgetManager.getInstance(ctx),
|
||||||
|
ids: IntArray = mgr.getAppWidgetIds(ComponentName(ctx, VotdWidget::class.java))) {
|
||||||
|
if (ids.isEmpty()) return
|
||||||
|
val v = Today.get(ctx)
|
||||||
|
for (id in ids) {
|
||||||
|
val rv = RemoteViews(ctx.packageName, R.layout.widget_votd)
|
||||||
|
rv.setTextViewText(R.id.w_text, "“" + v.text + "”")
|
||||||
|
rv.setTextViewText(R.id.w_ref, v.label)
|
||||||
|
rv.setOnClickPendingIntent(R.id.w_root, Today.openIntent(ctx, v.ref, 1))
|
||||||
|
mgr.updateAppWidget(id, rv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshAll(ctx: Context) = try { update(ctx) } catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object Daily {
|
||||||
|
private const val CHANNEL = "votd"
|
||||||
|
|
||||||
|
fun schedule(ctx: Context) {
|
||||||
|
val s = Settings.get(ctx)
|
||||||
|
val am = ctx.getSystemService(AlarmManager::class.java)
|
||||||
|
val pi = PendingIntent.getBroadcast(ctx, 7, Intent(ctx, DailyReceiver::class.java), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
am.cancel(pi)
|
||||||
|
val cal = Calendar.getInstance().apply {
|
||||||
|
set(Calendar.HOUR_OF_DAY, if (s.notify) s.notifyHour else 0)
|
||||||
|
set(Calendar.MINUTE, if (s.notify) s.notifyMinute else 5)
|
||||||
|
set(Calendar.SECOND, 0)
|
||||||
|
if (timeInMillis <= System.currentTimeMillis()) add(Calendar.DAY_OF_YEAR, 1)
|
||||||
|
}
|
||||||
|
// daily tick: refreshes the widget and (if turned on) posts the verse notification
|
||||||
|
am.setInexactRepeating(AlarmManager.RTC_WAKEUP, cal.timeInMillis, AlarmManager.INTERVAL_DAY, pi)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notifyNow(ctx: Context) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 33 &&
|
||||||
|
ContextCompat.checkSelfPermission(ctx, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) return
|
||||||
|
val nm = ctx.getSystemService(NotificationManager::class.java)
|
||||||
|
nm.createNotificationChannel(NotificationChannel(CHANNEL, "Verse of the day", NotificationManager.IMPORTANCE_DEFAULT))
|
||||||
|
val v = Today.get(ctx)
|
||||||
|
val n = NotificationCompat.Builder(ctx, CHANNEL)
|
||||||
|
.setSmallIcon(R.drawable.ic_notify)
|
||||||
|
.setContentTitle(v.label)
|
||||||
|
.setContentText(v.text)
|
||||||
|
.setStyle(NotificationCompat.BigTextStyle().bigText("“" + v.text + "”"))
|
||||||
|
.setContentIntent(Today.openIntent(ctx, v.ref, 2))
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.build()
|
||||||
|
nm.notify(1, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DailyReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(ctx: Context, intent: Intent) {
|
||||||
|
VotdWidget.refreshAll(ctx)
|
||||||
|
if (Settings.get(ctx).notify) Daily.notifyNow(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(ctx: Context, intent: Intent) {
|
||||||
|
Daily.schedule(ctx)
|
||||||
|
VotdWidget.refreshAll(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user