package com.vgmlr.stave
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Color as AndroidColor
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.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.outlined.Login
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.outlined.Block
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Park
import androidx.compose.material.icons.outlined.Place
import androidx.compose.material.icons.outlined.Receipt
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.dp
import androidx.compose.material.icons.outlined.WarningAmber
import kotlinx.coroutines.delay
import androidx.compose.ui.platform.LocalUriHandler
private val GRID_GAP = 12.dp
private val ARMED_BG = Color(0xFF880000)
private const val ARM_MS = 3000L
class StaveTrips : ComponentActivity() {
private lateinit var db: StaveDatabase
private lateinit var themeManager: StaveThemeManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.dark(AndroidColor.TRANSPARENT),
navigationBarStyle = SystemBarStyle.dark(AndroidColor.TRANSPARENT)
)
window.isNavigationBarContrastEnforced = false
db = StaveDatabase(this)
themeManager = StaveThemeManager(this)
setContent {
StaveTheme(themeManager) {
TripsScreen()
}
}
}
private fun openStave(id: Int) {
StaveSession.setCurrentTripId(this, id)
startActivity(
Intent(this, StaveMainActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
)
finish()
}
@OptIn(ExperimentalMaterial3Api::class)
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun TripsScreen() {
var trips by remember { mutableStateOf(db.getAllTrips()) }
var marked by remember { mutableStateOf<Int?>(null) }
var armed by remember { mutableStateOf(false) }
var showVersion by remember { mutableStateOf(false) }
val haptic = LocalHapticFeedback.current
val uriHandler = LocalUriHandler.current
val markedTrip = trips.firstOrNull { it.id == marked }
val markedEntries = remember(markedTrip) {
markedTrip?.let { db.getEntries(it.id) }.orEmpty()
}
val canBack = remember(trips) {
db.getTrip(StaveSession.currentTripId(this@StaveTrips)) != null
}
val importer = rememberLauncherForActivityResult(
ActivityResultContracts.OpenDocument()
) { uri ->
if (uri == null) return@rememberLauncherForActivityResult
if (db.importFile(this@StaveTrips, uri) != null) {
trips = db.getAllTrips()
} else {
Toast.makeText(this@StaveTrips, "Import failed", Toast.LENGTH_SHORT).show()
}
}
BackHandler(enabled = marked != null) { marked = null }
LaunchedEffect(marked) { armed = false }
LaunchedEffect(armed) {
if (armed) {
delay(ARM_MS)
armed = false
}
}
Scaffold(
topBar = {
TopAppBar(
title = { },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent
),
actions = {
Spacer(Modifier.width(BAR_EDGE))
StaveButton(
Icons.AutoMirrored.Filled.ArrowBack,
"Back",
enabled = canBack
) { finish() }
Spacer(Modifier.width(BAR_GAP))
StaveBlankButton(
Modifier.weight(1f),
label = "stave",
labelAlignment = Alignment.CenterEnd
)
Spacer(Modifier.width(BAR_GAP))
StaveButton(Icons.Default.Add, "New Stave") {
openStave(db.addTrip("", StaveOTCClock.getCurrentOTCTime()).toInt())
}
Spacer(Modifier.width(BAR_GAP))
Box {
var expanded by remember { mutableStateOf(false) }
StaveButton(Icons.Outlined.Receipt, "Menu") { expanded = true }
StaveMenu(expanded = expanded, onDismiss = { expanded = false }) {
StaveMenuItem("Import", Icons.AutoMirrored.Outlined.Login) {
importer.launch(arrayOf("text/*", "application/octet-stream"))
expanded = false
}
StaveMenuItem("Settings", Icons.Outlined.Settings) {
startActivity(Intent(this@StaveTrips, StaveSettings::class.java))
expanded = false
}
StaveMenuItem("Version", Icons.Outlined.Park) {
showVersion = true; expanded = false
}
StaveMenuItem("Exit", Icons.Outlined.Block, enabled = false) { }
}
}
Spacer(Modifier.width(BAR_EDGE_ACTIONS))
}
)
},
bottomBar = {
markedTrip?.let { t ->
Box(Modifier.fillMaxWidth()) {
StaveDock {
StaveMenuItem(
"Place",
Icons.Outlined.Place,
enabled = markedEntries.isNotEmpty()
) { StaveMap.open(this@StaveTrips, t.id) }
StaveMenuItem(
"Share",
Icons.Default.Share,
enabled = markedEntries.isNotEmpty()
) {
shareStave(this@StaveTrips, t, markedEntries)
}
StaveMenuItem(
"Delete",
if (armed) Icons.Outlined.WarningAmber else Icons.Outlined.Delete,
iconSize = BTN_ICON + 2.dp,
bg = if (armed) ARMED_BG else null
) {
if (!armed) {
armed = true
} else {
armed = false
marked = null
if (StaveSession.currentTripId(this@StaveTrips) == t.id) {
StaveSession.clear(this@StaveTrips)
}
db.deleteTrip(t.id)
trips = db.getAllTrips()
}
}
}
}
}
},
contentWindowInsets = WindowInsets(0, 0, 0, 0),
containerColor = themeManager.backgroundColor.value
) { _ ->
val topInset = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
val navInset = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = PaddingValues(
start = BAR_EDGE,
end = BAR_EDGE,
top = topInset + TOP_BAR_H + 4.dp,
bottom = navInset + BTN_SIZE + DOCK_PAD * 2
),
horizontalArrangement = Arrangement.spacedBy(GRID_GAP),
verticalArrangement = Arrangement.spacedBy(GRID_GAP),
modifier = Modifier
.fillMaxSize()
.background(themeManager.backgroundColor.value)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) { marked = null }
) {
items(trips, key = { it.id }) { trip ->
TripCard(
trip = trip,
selected = marked == trip.id,
themeManager = themeManager,
onClick = { if (marked != null) marked = null else openStave(trip.id) },
onLongClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
marked = trip.id
}
)
}
}
}
VersionDialog(showVersion, onDismiss = { showVersion = false }, uriHandler = uriHandler)
}
}