187 lines
7.4 KiB
Kotlin
187 lines
7.4 KiB
Kotlin
package space.bloggin.vellum
|
|
|
|
import android.content.ClipData
|
|
import android.content.ClipboardManager
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.os.Bundle
|
|
import android.widget.Toast
|
|
import androidx.activity.ComponentActivity
|
|
import androidx.activity.SystemBarStyle
|
|
import androidx.activity.compose.BackHandler
|
|
import androidx.activity.compose.setContent
|
|
import androidx.activity.enableEdgeToEdge
|
|
import androidx.compose.material3.MaterialTheme
|
|
import androidx.compose.material3.lightColorScheme
|
|
import androidx.compose.material3.darkColorScheme
|
|
import androidx.compose.runtime.*
|
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
|
import androidx.core.view.WindowCompat
|
|
|
|
sealed class Screen {
|
|
data object GoTo : Screen()
|
|
data object Search : Screen()
|
|
data class Library(val tab: Int = 0) : Screen()
|
|
data object PlansScreen : Screen()
|
|
data object TopicsScreen : Screen()
|
|
data class TopicDetail(val topic: Topic) : Screen()
|
|
data object Appearance : Screen()
|
|
data object SettingsScreen : Screen()
|
|
data object About : Screen()
|
|
data class ShareImg(val text: String, val label: String) : Screen()
|
|
}
|
|
|
|
sealed class Sheet {
|
|
data class StrongsWords(val ref: Ref) : Sheet()
|
|
data class StrongsDef(val id: String, val word: String) : Sheet()
|
|
data class Xrefs(val ref: Ref) : Sheet()
|
|
data class Compare(val refs: List<Ref>) : Sheet()
|
|
data class NoteEdit(val ref: Ref) : Sheet()
|
|
data object ReadAloud : Sheet()
|
|
data object AutoScroll : Sheet()
|
|
data object Translations : Sheet()
|
|
}
|
|
|
|
class AppState(val ctx: Context, val bible: Bible, val user: UserData, val settings: Settings, val speech: Speech) {
|
|
val screens: SnapshotStateList<Screen> = mutableStateListOf()
|
|
var sheet by mutableStateOf<Sheet?>(null)
|
|
var sheetStack = mutableStateListOf<Sheet>()
|
|
val selection: SnapshotStateList<Ref> = mutableStateListOf()
|
|
/** a jump the reader should perform; reader clears it */
|
|
var jump by mutableStateOf<Ref?>(null)
|
|
var jumpTop by mutableStateOf(false)
|
|
/** briefly glows after a jump */
|
|
var flash by mutableStateOf<Ref?>(null)
|
|
var autoScroll by mutableStateOf(false)
|
|
var drawerOpen by mutableStateOf(false)
|
|
/** current chapter shown at the top of the reader */
|
|
var currentBook by mutableIntStateOf(1)
|
|
var currentChapter by mutableIntStateOf(1)
|
|
var currentVerse by mutableIntStateOf(1)
|
|
|
|
init {
|
|
val last = settings.lastItem
|
|
if (last in 1 until bible.items.size) {
|
|
val it = bible.items[last]
|
|
currentBook = maxOf(1, Item.book(it)); currentChapter = maxOf(1, Item.chapter(it)); currentVerse = maxOf(1, Item.verse(it))
|
|
}
|
|
}
|
|
|
|
fun open(s: Screen) { drawerOpen = false; screens.add(s) }
|
|
fun back(): Boolean {
|
|
when {
|
|
sheet != null -> { sheet = if (sheetStack.isNotEmpty()) sheetStack.removeAt(sheetStack.lastIndex) else null }
|
|
drawerOpen -> drawerOpen = false
|
|
screens.isNotEmpty() -> screens.removeAt(screens.lastIndex)
|
|
selection.isNotEmpty() -> selection.clear()
|
|
autoScroll -> autoScroll = false
|
|
else -> return false
|
|
}
|
|
return true
|
|
}
|
|
fun showSheet(s: Sheet, stack: Boolean = false) {
|
|
if (stack && sheet != null) sheetStack.add(sheet!!) else sheetStack.clear()
|
|
sheet = s
|
|
}
|
|
fun closeSheet() { sheetStack.clear(); sheet = null }
|
|
|
|
fun goTo(r: Ref) {
|
|
screens.clear(); sheet = null; sheetStack.clear(); drawerOpen = false
|
|
jump = r
|
|
if (r.verse > 0) flash = r
|
|
}
|
|
fun goTop() { screens.clear(); sheet = null; drawerOpen = false; jumpTop = true }
|
|
|
|
// ------------ selection helpers -------------
|
|
fun sortedSelection() = selection.sortedBy { it.key() }
|
|
|
|
fun selectionLabel(): String = labelFor(sortedSelection())
|
|
|
|
fun labelFor(refs: List<Ref>): String {
|
|
if (refs.isEmpty()) return ""
|
|
val first = refs.first()
|
|
val sameChapter = refs.all { it.book == first.book && it.chapter == first.chapter }
|
|
if (!sameChapter) return refs.joinToString("; ") { bible.refLabel(it) }
|
|
val vs = refs.map { it.verse }
|
|
val parts = ArrayList<String>()
|
|
var start = vs[0]; var prev = vs[0]
|
|
for (v in vs.drop(1) + listOf(-1)) {
|
|
if (v == prev + 1) { prev = v; continue }
|
|
parts.add(if (start == prev) "$start" else "$start-$prev")
|
|
start = v; prev = v
|
|
}
|
|
return bible.book(first.book).name + " " + first.chapter + ":" + parts.joinToString(",")
|
|
}
|
|
|
|
fun textFor(refs: List<Ref>, tr: Int = settings.tr): String =
|
|
refs.joinToString(" ") { bible.plainVerse(tr, it.book, it.chapter, it.verse) }.trim()
|
|
|
|
fun copy(text: String, label: String) {
|
|
val cm = ctx.getSystemService(ClipboardManager::class.java)
|
|
cm.setPrimaryClip(ClipData.newPlainText(label, "“$text”\n— $label (${bible.trCode(settings.tr)})"))
|
|
Toast.makeText(ctx, "Copied $label", Toast.LENGTH_SHORT).show()
|
|
}
|
|
|
|
fun shareText(text: String, label: String) {
|
|
val i = Intent(Intent.ACTION_SEND).apply {
|
|
type = "text/plain"
|
|
putExtra(Intent.EXTRA_TEXT, "“$text”\n— $label (${bible.trCode(settings.tr)})")
|
|
}
|
|
ctx.startActivity(Intent.createChooser(i, "Share verse").addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
|
}
|
|
|
|
fun toast(s: String) = Toast.makeText(ctx, s, Toast.LENGTH_SHORT).show()
|
|
}
|
|
|
|
class MainActivity : ComponentActivity() {
|
|
lateinit var state: AppState
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
val settings = Settings.get(this)
|
|
val bible = Bible.get(this)
|
|
val user = UserData.get(this)
|
|
val speech = Speech(this, bible, settings)
|
|
state = AppState(this, bible, user, settings, speech)
|
|
handleIntent(intent, first = true)
|
|
Daily.schedule(this)
|
|
VotdWidget.refreshAll(this)
|
|
|
|
setContent {
|
|
val bd = settings.activeBg
|
|
LaunchedEffect(bd.dark) {
|
|
enableEdgeToEdge(
|
|
statusBarStyle = if (bd.dark) SystemBarStyle.dark(0) else SystemBarStyle.light(0, 0),
|
|
navigationBarStyle = if (bd.dark) SystemBarStyle.dark(0) else SystemBarStyle.light(0, 0),
|
|
)
|
|
}
|
|
val scheme = if (bd.dark) darkColorScheme(primary = bd.ink, surface = bd.panel, onSurface = bd.ink, background = bd.panel)
|
|
else lightColorScheme(primary = bd.ink, surface = bd.panel, onSurface = bd.ink, background = bd.panel)
|
|
MaterialTheme(colorScheme = scheme) {
|
|
CompositionLocalProvider(LocalBackdrop provides bd) {
|
|
BackHandler(enabled = state.sheet != null || state.drawerOpen || state.screens.isNotEmpty() || state.selection.isNotEmpty() || state.autoScroll) {
|
|
state.back()
|
|
}
|
|
App(state)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onNewIntent(intent: Intent) {
|
|
super.onNewIntent(intent)
|
|
handleIntent(intent, first = false)
|
|
}
|
|
|
|
private fun handleIntent(i: Intent?, first: Boolean) {
|
|
val k = i?.getIntExtra("ref", 0) ?: 0
|
|
if (k > 0) state.goTo(Ref.fromKey(k))
|
|
else if (first && state.settings.openAtVotd) state.jumpTop = true
|
|
}
|
|
|
|
override fun onDestroy() {
|
|
state.speech.shutdown()
|
|
super.onDestroy()
|
|
}
|
|
}
|