add settings screen, clock transparent bg toggle, event tap-to-open, readme

This commit is contained in:
Damocles 2026-04-22 22:26:34 +02:00
parent ca9c45cba1
commit a1a80905a1
8 changed files with 413 additions and 8 deletions

View file

@ -6,16 +6,23 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import space.darkest.nova.android.data.AgendaDay
import space.darkest.nova.android.data.CalendarRepository
import space.darkest.nova.android.ui.AgendaScreen
import space.darkest.nova.android.ui.SettingsScreen
import space.darkest.nova.android.ui.theme.NovaTheme
class MainActivity : ComponentActivity() {
private var agenda by mutableStateOf(emptyList<space.darkest.nova.android.data.AgendaDay>())
private var agenda by mutableStateOf(emptyList<AgendaDay>())
private val calendarPermission = registerForActivityResult(
ActivityResultContracts.RequestPermission()
@ -31,7 +38,25 @@ class MainActivity : ComponentActivity() {
setContent {
NovaTheme {
AgendaScreen(agenda)
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "agenda",
enterTransition = { slideInHorizontally { it } },
exitTransition = { slideOutHorizontally { -it } },
popEnterTransition = { slideInHorizontally { -it } },
popExitTransition = { slideOutHorizontally { it } },
) {
composable("agenda") {
AgendaScreen(
agenda = agenda,
onSettingsClick = { navController.navigate("settings") },
)
}
composable("settings") {
SettingsScreen(onBack = { navController.popBackStack() })
}
}
}
}
}

View file

@ -0,0 +1,20 @@
package space.darkest.nova.android.data
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
object WidgetPreferences {
private const val PREFS_NAME = "widget_prefs"
private const val KEY_CLOCK_TRANSPARENT_BG = "clock_transparent_bg"
private fun prefs(context: Context): SharedPreferences =
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
fun clockTransparentBg(context: Context): Boolean =
prefs(context).getBoolean(KEY_CLOCK_TRANSPARENT_BG, false)
fun setClockTransparentBg(context: Context, value: Boolean) {
prefs(context).edit { putBoolean(KEY_CLOCK_TRANSPARENT_BG, value) }
}
}

View file

@ -18,10 +18,12 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.IconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme
@ -47,7 +49,7 @@ import java.time.format.TextStyle
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AgendaScreen(agenda: List<AgendaDay>) {
fun AgendaScreen(agenda: List<AgendaDay>, onSettingsClick: () -> Unit = {}) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
val context = LocalContext.current
@ -55,6 +57,11 @@ fun AgendaScreen(agenda: List<AgendaDay>) {
topBar = {
LargeTopAppBar(
title = { Text("Agenda") },
actions = {
IconButton(onClick = onSettingsClick) {
Icon(Icons.Default.Settings, contentDescription = "Widget settings")
}
},
scrollBehavior = scrollBehavior,
)
},
@ -136,7 +143,16 @@ private fun DayHeader(date: LocalDate) {
@Composable
private fun EventCard(event: AgendaEvent) {
val context = LocalContext.current
Card(
onClick = {
context.startActivity(Intent(Intent.ACTION_VIEW).apply {
data = android.net.Uri.withAppendedPath(
CalendarContract.Events.CONTENT_URI,
event.id.toString(),
)
})
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp),

View file

@ -0,0 +1,284 @@
package space.darkest.nova.android.ui
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import space.darkest.nova.android.data.WidgetPreferences
import space.darkest.nova.android.widget.ClockWidgetReceiver
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(onBack: () -> Unit) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
var showClockSheet by remember { mutableStateOf(false) }
Scaffold(
topBar = {
LargeTopAppBar(
title = { Text("Widgets") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
},
scrollBehavior = scrollBehavior,
)
},
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { innerPadding ->
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = innerPadding,
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
item { Spacer(Modifier.height(4.dp)) }
item {
WidgetPreviewCard(
title = "Clock",
description = "Dual-color time display with next alarm",
onClick = { showClockSheet = true },
) {
ClockWidgetPreview()
}
}
item {
WidgetPreviewCard(
title = "Agenda",
description = "Calendar events for the next 2 weeks",
onClick = { /* no settings yet */ },
) {
AgendaWidgetPreview()
}
}
item { Spacer(Modifier.height(16.dp)) }
}
}
if (showClockSheet) {
ClockSettingsSheet(onDismiss = { showClockSheet = false })
}
}
@Composable
private fun WidgetPreviewCard(
title: String,
description: String,
onClick: () -> Unit,
preview: @Composable () -> Unit,
) {
Card(
onClick = onClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
),
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium,
)
Text(
text = description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(Modifier.height(12.dp))
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
),
) {
Column(modifier = Modifier.padding(16.dp).fillMaxWidth()) {
preview()
}
}
}
}
}
@Composable
private fun ClockWidgetPreview() {
val context = LocalContext.current
val transparentBg = remember { WidgetPreferences.clockTransparentBg(context) }
Column {
Row(verticalAlignment = Alignment.Bottom) {
Text(
text = "14",
style = MaterialTheme.typography.displayLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
)
Text(
text = ":",
style = MaterialTheme.typography.displayLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
text = "37",
style = MaterialTheme.typography.displayLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.tertiary,
)
}
Text(
text = "Tue, 22 Apr",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
@Composable
private fun AgendaWidgetPreview() {
Column {
Text(
text = "Today",
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Medium,
)
Spacer(Modifier.height(8.dp))
Row {
Text(
text = "09:00",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.width(44.dp),
)
Spacer(Modifier.width(8.dp))
Text(
text = "Team standup",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
)
}
Spacer(Modifier.height(4.dp))
Row {
Text(
text = "14:00",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.width(44.dp),
)
Spacer(Modifier.width(8.dp))
Text(
text = "Design review",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ClockSettingsSheet(onDismiss: () -> Unit) {
val context = LocalContext.current
val sheetState = rememberModalBottomSheetState()
var transparentBg by remember { mutableStateOf(WidgetPreferences.clockTransparentBg(context)) }
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.padding(bottom = 32.dp),
) {
Text(
text = "Clock widget",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Medium,
)
Spacer(Modifier.height(20.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Transparent background",
style = MaterialTheme.typography.bodyLarge,
)
Text(
text = "Remove the widget background",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Switch(
checked = transparentBg,
onCheckedChange = {
transparentBg = it
WidgetPreferences.setClockTransparentBg(context, it)
requestClockWidgetUpdate(context)
},
)
}
}
}
}
private fun requestClockWidgetUpdate(context: Context) {
val manager = AppWidgetManager.getInstance(context)
val component = ComponentName(context, ClockWidgetReceiver::class.java)
val ids = manager.getAppWidgetIds(component)
if (ids.isNotEmpty()) {
context.sendBroadcast(
Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE).apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
setComponent(component)
}
)
}
}

View file

@ -4,7 +4,6 @@ import android.app.AlarmManager
import android.content.Context
import android.content.Intent
import android.provider.AlarmClock
import android.provider.Settings
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@ -17,7 +16,9 @@ import androidx.glance.appwidget.GlanceAppWidgetReceiver
import androidx.glance.appwidget.action.actionStartActivity
import androidx.glance.appwidget.provideContent
import androidx.glance.background
import androidx.glance.color.ColorProvider
import androidx.glance.color.DynamicThemeColorProviders
import space.darkest.nova.android.data.WidgetPreferences
import androidx.glance.layout.Alignment
import androidx.glance.layout.Box
import androidx.glance.layout.Column
@ -47,16 +48,17 @@ class ClockWidget : GlanceAppWidget() {
.atZone(ZoneId.systemDefault())
.toLocalDateTime()
}
val transparentBg = WidgetPreferences.clockTransparentBg(context)
provideContent {
GlanceTheme(colors = DynamicThemeColorProviders) {
ClockContent(nextAlarm?.toLocalTime(), nextAlarm?.toLocalDate())
ClockContent(nextAlarm?.toLocalTime(), nextAlarm?.toLocalDate(), transparentBg)
}
}
}
@Composable
private fun ClockContent(alarmTime: LocalTime?, alarmDate: LocalDate?) {
private fun ClockContent(alarmTime: LocalTime?, alarmDate: LocalDate?, transparentBg: Boolean) {
val now = LocalTime.now()
val today = LocalDate.now()
val hours = now.format(DateTimeFormatter.ofPattern("HH"))
@ -67,10 +69,14 @@ class ClockWidget : GlanceAppWidget() {
"$day, $date"
}
val bgModifier = if (transparentBg) {
GlanceModifier
} else {
GlanceModifier.background(GlanceTheme.colors.widgetBackground)
}
Box(
modifier = GlanceModifier
modifier = bgModifier
.fillMaxSize()
.background(GlanceTheme.colors.widgetBackground)
.padding(16.dp)
.clickable(actionStartActivity(Intent(AlarmClock.ACTION_SHOW_ALARMS))),
contentAlignment = Alignment.CenterStart,