diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 1dbc5b7..ed3cbda 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,14 +1,12 @@
plugins {
alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.compose.compiler)
}
android {
namespace = "space.darkest.nova.android"
- compileSdk {
- version = release(36) {
- minorApiLevel = 1
- }
- }
+ compileSdk = 36
defaultConfig {
applicationId = "space.darkest.nova.android"
@@ -22,24 +20,46 @@ android {
buildTypes {
release {
- isMinifyEnabled = false
+ isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
+
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+
+ buildFeatures {
+ compose = true
}
}
dependencies {
- implementation(libs.androidx.appcompat)
implementation(libs.androidx.core.ktx)
- implementation(libs.material)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material3)
+ implementation(libs.androidx.material.icons)
+
+ implementation(libs.androidx.glance)
+ implementation(libs.androidx.glance.m3)
+
+ debugImplementation(libs.androidx.ui.tooling)
+
testImplementation(libs.junit)
- androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.androidx.junit)
-}
\ No newline at end of file
+ androidTestImplementation(libs.androidx.espresso.core)
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 47439d4..da69fa5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,6 +2,8 @@
+
+
+ android:theme="@style/Theme.NovaShellForAndroid">
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/space/darkest/nova/android/MainActivity.kt b/app/src/main/java/space/darkest/nova/android/MainActivity.kt
new file mode 100644
index 0000000..525790a
--- /dev/null
+++ b/app/src/main/java/space/darkest/nova/android/MainActivity.kt
@@ -0,0 +1,47 @@
+package space.darkest.nova.android
+
+import android.Manifest
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import space.darkest.nova.android.data.CalendarRepository
+import space.darkest.nova.android.ui.AgendaScreen
+import space.darkest.nova.android.ui.theme.NovaTheme
+
+class MainActivity : ComponentActivity() {
+
+ private var agenda by mutableStateOf(emptyList())
+
+ private val calendarPermission = registerForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { granted ->
+ if (granted) refreshAgenda()
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+
+ calendarPermission.launch(Manifest.permission.READ_CALENDAR)
+
+ setContent {
+ NovaTheme {
+ AgendaScreen(agenda)
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ refreshAgenda()
+ }
+
+ private fun refreshAgenda() {
+ agenda = CalendarRepository.getAgenda(this, 14)
+ }
+}
diff --git a/app/src/main/java/space/darkest/nova/android/data/CalendarRepository.kt b/app/src/main/java/space/darkest/nova/android/data/CalendarRepository.kt
new file mode 100644
index 0000000..64b9c9c
--- /dev/null
+++ b/app/src/main/java/space/darkest/nova/android/data/CalendarRepository.kt
@@ -0,0 +1,90 @@
+package space.darkest.nova.android.data
+
+import android.Manifest
+import android.content.ContentUris
+import android.content.Context
+import android.content.pm.PackageManager
+import android.provider.CalendarContract
+import java.time.Instant
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.ZoneId
+import java.time.temporal.ChronoUnit
+
+data class AgendaEvent(
+ val id: Long,
+ val title: String,
+ val startTime: LocalDateTime,
+ val endTime: LocalDateTime,
+ val allDay: Boolean,
+ val calendarColor: Int,
+ val location: String?,
+)
+
+data class AgendaDay(
+ val date: LocalDate,
+ val events: List,
+)
+
+object CalendarRepository {
+
+ fun getAgenda(context: Context, days: Int = 14): List {
+ if (context.checkSelfPermission(Manifest.permission.READ_CALENDAR)
+ != PackageManager.PERMISSION_GRANTED
+ ) return emptyList()
+
+ val zone = ZoneId.systemDefault()
+ val now = Instant.now()
+ val start = now.toEpochMilli()
+ val end = now.plus(days.toLong(), ChronoUnit.DAYS).toEpochMilli()
+
+ val uri = CalendarContract.Instances.CONTENT_URI.buildUpon().let {
+ ContentUris.appendId(it, start)
+ ContentUris.appendId(it, end)
+ it.build()
+ }
+
+ val projection = arrayOf(
+ CalendarContract.Instances.EVENT_ID,
+ CalendarContract.Instances.TITLE,
+ CalendarContract.Instances.BEGIN,
+ CalendarContract.Instances.END,
+ CalendarContract.Instances.ALL_DAY,
+ CalendarContract.Instances.CALENDAR_COLOR,
+ CalendarContract.Instances.EVENT_LOCATION,
+ )
+
+ val events = mutableListOf()
+
+ context.contentResolver.query(
+ uri, projection, null, null, "${CalendarContract.Instances.BEGIN} ASC"
+ )?.use { cursor ->
+ val idIdx = cursor.getColumnIndex(CalendarContract.Instances.EVENT_ID)
+ val titleIdx = cursor.getColumnIndex(CalendarContract.Instances.TITLE)
+ val beginIdx = cursor.getColumnIndex(CalendarContract.Instances.BEGIN)
+ val endIdx = cursor.getColumnIndex(CalendarContract.Instances.END)
+ val allDayIdx = cursor.getColumnIndex(CalendarContract.Instances.ALL_DAY)
+ val colorIdx = cursor.getColumnIndex(CalendarContract.Instances.CALENDAR_COLOR)
+ val locIdx = cursor.getColumnIndex(CalendarContract.Instances.EVENT_LOCATION)
+
+ while (cursor.moveToNext()) {
+ events += AgendaEvent(
+ id = cursor.getLong(idIdx),
+ title = cursor.getString(titleIdx) ?: "(No title)",
+ startTime = Instant.ofEpochMilli(cursor.getLong(beginIdx))
+ .atZone(zone).toLocalDateTime(),
+ endTime = Instant.ofEpochMilli(cursor.getLong(endIdx))
+ .atZone(zone).toLocalDateTime(),
+ allDay = cursor.getInt(allDayIdx) == 1,
+ calendarColor = cursor.getInt(colorIdx),
+ location = cursor.getString(locIdx)?.takeIf { it.isNotBlank() },
+ )
+ }
+ }
+
+ return events
+ .groupBy { it.startTime.toLocalDate() }
+ .map { (date, dayEvents) -> AgendaDay(date, dayEvents) }
+ .sortedBy { it.date }
+ }
+}
diff --git a/app/src/main/java/space/darkest/nova/android/ui/AgendaScreen.kt b/app/src/main/java/space/darkest/nova/android/ui/AgendaScreen.kt
new file mode 100644
index 0000000..10dcb50
--- /dev/null
+++ b/app/src/main/java/space/darkest/nova/android/ui/AgendaScreen.kt
@@ -0,0 +1,199 @@
+package space.darkest.nova.android.ui
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+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.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.LargeTopAppBar
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import space.darkest.nova.android.data.AgendaDay
+import space.darkest.nova.android.data.AgendaEvent
+import java.time.LocalDate
+import java.time.format.DateTimeFormatter
+import java.time.format.TextStyle
+import java.util.Locale
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AgendaScreen(agenda: List) {
+ val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
+
+ Scaffold(
+ topBar = {
+ LargeTopAppBar(
+ title = { Text("Agenda") },
+ scrollBehavior = scrollBehavior,
+ )
+ },
+ modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+ ) { innerPadding ->
+ if (agenda.isEmpty()) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding),
+ contentAlignment = Alignment.Center,
+ ) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(
+ text = "No upcoming events",
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ Spacer(Modifier.height(4.dp))
+ Text(
+ text = "Your next 2 weeks are clear",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ }
+ } else {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = innerPadding,
+ ) {
+ for (day in agenda) {
+ item(key = "header-${day.date}") {
+ DayHeader(day.date)
+ }
+ items(day.events, key = { "${it.id}-${it.startTime}" }) { event ->
+ EventCard(event)
+ }
+ }
+ item { Spacer(Modifier.height(16.dp)) }
+ }
+ }
+ }
+}
+
+@Composable
+private fun DayHeader(date: LocalDate) {
+ val today = LocalDate.now()
+ val label = when (date) {
+ today -> "Today"
+ today.plusDays(1) -> "Tomorrow"
+ else -> {
+ val dayName = date.dayOfWeek.getDisplayName(TextStyle.FULL, Locale.getDefault())
+ val monthDay = date.format(DateTimeFormatter.ofPattern("MMM d"))
+ "$dayName, $monthDay"
+ }
+ }
+
+ Text(
+ text = label,
+ modifier = Modifier.padding(start = 72.dp, top = 20.dp, bottom = 8.dp, end = 16.dp),
+ style = MaterialTheme.typography.labelLarge,
+ color = MaterialTheme.colorScheme.primary,
+ fontWeight = FontWeight.Medium,
+ )
+}
+
+@Composable
+private fun EventCard(event: AgendaEvent) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 4.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
+ ),
+ shape = RoundedCornerShape(12.dp),
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(IntrinsicSize.Min)
+ .padding(12.dp),
+ verticalAlignment = Alignment.Top,
+ ) {
+ // time column
+ Column(
+ modifier = Modifier.width(52.dp),
+ horizontalAlignment = Alignment.End,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ if (event.allDay) {
+ Text(
+ text = "All day",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ } else {
+ Text(
+ text = event.startTime.format(timeFormatter),
+ style = MaterialTheme.typography.bodyMedium,
+ fontWeight = FontWeight.Medium,
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ Text(
+ text = event.endTime.format(timeFormatter),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ }
+
+ Spacer(Modifier.width(12.dp))
+
+ // color bar
+ Box(
+ modifier = Modifier
+ .width(4.dp)
+ .fillMaxHeight()
+ .clip(RoundedCornerShape(2.dp))
+ .background(Color(event.calendarColor)),
+ )
+
+ Spacer(Modifier.width(12.dp))
+
+ // details
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = event.title,
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.Medium,
+ color = MaterialTheme.colorScheme.onSurface,
+ maxLines = 2,
+ )
+ if (event.location != null) {
+ Spacer(Modifier.height(2.dp))
+ Text(
+ text = event.location,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 1,
+ )
+ }
+ }
+ }
+ }
+}
+
+private val timeFormatter = DateTimeFormatter.ofPattern("HH:mm")
diff --git a/app/src/main/java/space/darkest/nova/android/ui/theme/Theme.kt b/app/src/main/java/space/darkest/nova/android/ui/theme/Theme.kt
new file mode 100644
index 0000000..5862ce1
--- /dev/null
+++ b/app/src/main/java/space/darkest/nova/android/ui/theme/Theme.kt
@@ -0,0 +1,26 @@
+package space.darkest.nova.android.ui.theme
+
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+@Composable
+fun NovaTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ content: @Composable () -> Unit,
+) {
+ val context = LocalContext.current
+ val colorScheme = if (darkTheme) {
+ dynamicDarkColorScheme(context)
+ } else {
+ dynamicLightColorScheme(context)
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ content = content,
+ )
+}
diff --git a/app/src/main/java/space/darkest/nova/android/widget/AgendaWidget.kt b/app/src/main/java/space/darkest/nova/android/widget/AgendaWidget.kt
new file mode 100644
index 0000000..1ff08fa
--- /dev/null
+++ b/app/src/main/java/space/darkest/nova/android/widget/AgendaWidget.kt
@@ -0,0 +1,254 @@
+package space.darkest.nova.android.widget
+
+import android.content.Context
+import android.content.Intent
+import android.provider.CalendarContract
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.glance.GlanceId
+import androidx.glance.GlanceModifier
+import androidx.glance.GlanceTheme
+import androidx.glance.action.actionStartActivity
+import androidx.glance.action.clickable
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.GlanceAppWidgetReceiver
+import androidx.glance.appwidget.action.actionStartActivity as actionStartActivityIntent
+import androidx.glance.appwidget.components.Scaffold
+import androidx.glance.appwidget.components.TitleBar
+import androidx.glance.appwidget.cornerRadius
+import androidx.glance.appwidget.lazy.LazyColumn
+import androidx.glance.appwidget.lazy.items
+import androidx.glance.appwidget.provideContent
+import androidx.glance.background
+import androidx.glance.color.DynamicThemeColorProviders
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Box
+import androidx.glance.layout.Column
+import androidx.glance.layout.Row
+import androidx.glance.layout.Spacer
+import androidx.glance.layout.fillMaxSize
+import androidx.glance.layout.fillMaxWidth
+import androidx.glance.layout.height
+import androidx.glance.layout.padding
+import androidx.glance.layout.size
+import androidx.glance.layout.width
+import androidx.glance.text.FontWeight
+import androidx.glance.text.Text
+import androidx.glance.text.TextStyle
+import space.darkest.nova.android.MainActivity
+import space.darkest.nova.android.data.AgendaDay
+import space.darkest.nova.android.data.AgendaEvent
+import space.darkest.nova.android.data.CalendarRepository
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+import java.time.format.TextStyle as DateTextStyle
+import java.util.Locale
+
+class AgendaWidget : GlanceAppWidget() {
+
+ override suspend fun provideGlance(context: Context, id: GlanceId) {
+ val agenda = CalendarRepository.getAgenda(context, days = 14)
+
+ provideContent {
+ GlanceTheme(colors = DynamicThemeColorProviders) {
+ AgendaContent(agenda)
+ }
+ }
+ }
+
+ @Composable
+ private fun AgendaContent(agenda: List) {
+ Scaffold(
+ titleBar = TitleBar(
+ startIcon = null,
+ title = "Agenda",
+ textColor = GlanceTheme.colors.onSurface,
+ ),
+ backgroundColor = GlanceTheme.colors.widgetBackground,
+ modifier = GlanceModifier.clickable(actionStartActivity()),
+ ) {
+ if (agenda.isEmpty()) {
+ EmptyState()
+ } else {
+ AgendaList(agenda)
+ }
+ }
+ }
+
+ @Composable
+ private fun EmptyState() {
+ Box(
+ modifier = GlanceModifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(
+ text = "No upcoming events",
+ style = TextStyle(
+ color = GlanceTheme.colors.onSurfaceVariant,
+ fontSize = 14.sp,
+ ),
+ )
+ Spacer(GlanceModifier.height(4.dp))
+ Text(
+ text = "Your next 2 weeks are clear",
+ style = TextStyle(
+ color = GlanceTheme.colors.onSurfaceVariant,
+ fontSize = 12.sp,
+ ),
+ )
+ }
+ }
+ }
+
+ @Composable
+ private fun AgendaList(agenda: List) {
+ val items = buildList {
+ for (day in agenda) {
+ add(AgendaListItem.Header(day.date))
+ for (event in day.events) {
+ add(AgendaListItem.Event(event))
+ }
+ }
+ }
+
+ LazyColumn(modifier = GlanceModifier.fillMaxSize()) {
+ items(items, itemId = { item ->
+ when (item) {
+ is AgendaListItem.Header -> item.date.toEpochDay()
+ is AgendaListItem.Event -> item.event.id * 31 + item.event.startTime.hashCode()
+ }
+ }) { item ->
+ when (item) {
+ is AgendaListItem.Header -> DayHeader(item.date)
+ is AgendaListItem.Event -> EventCard(item.event)
+ }
+ }
+ }
+ }
+
+ @Composable
+ private fun DayHeader(date: LocalDate) {
+ val today = LocalDate.now()
+ val label = when (date) {
+ today -> "Today"
+ today.plusDays(1) -> "Tomorrow"
+ else -> {
+ val dayName = date.dayOfWeek.getDisplayName(DateTextStyle.FULL, Locale.getDefault())
+ val monthDay = date.format(DateTimeFormatter.ofPattern("MMM d"))
+ "$dayName, $monthDay"
+ }
+ }
+
+ Text(
+ text = label,
+ modifier = GlanceModifier.fillMaxWidth().padding(
+ start = 16.dp, end = 16.dp, top = 12.dp, bottom = 4.dp
+ ),
+ style = TextStyle(
+ color = GlanceTheme.colors.primary,
+ fontSize = 13.sp,
+ fontWeight = FontWeight.Medium,
+ ),
+ )
+ }
+
+ @Composable
+ private fun EventCard(event: AgendaEvent) {
+ val calendarIntent = Intent(Intent.ACTION_VIEW).apply {
+ data = CalendarContract.Events.CONTENT_URI.buildUpon()
+ .appendPath(event.id.toString()).build()
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+
+ Row(
+ modifier = GlanceModifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 4.dp)
+ .clickable(actionStartActivityIntent(calendarIntent)),
+ verticalAlignment = Alignment.Top,
+ ) {
+ // time column
+ Column(
+ modifier = GlanceModifier.width(52.dp),
+ horizontalAlignment = Alignment.End,
+ ) {
+ if (event.allDay) {
+ Text(
+ text = "All day",
+ style = TextStyle(
+ color = GlanceTheme.colors.onSurfaceVariant,
+ fontSize = 12.sp,
+ ),
+ )
+ } else {
+ Text(
+ text = event.startTime.format(timeFormatter),
+ style = TextStyle(
+ color = GlanceTheme.colors.onSurface,
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Medium,
+ ),
+ )
+ Text(
+ text = event.endTime.format(timeFormatter),
+ style = TextStyle(
+ color = GlanceTheme.colors.onSurfaceVariant,
+ fontSize = 11.sp,
+ ),
+ )
+ }
+ }
+
+ Spacer(GlanceModifier.width(8.dp))
+
+ // color indicator
+ Box(
+ modifier = GlanceModifier
+ .size(width = 3.dp, height = 36.dp)
+ .cornerRadius(2.dp)
+ .background(GlanceTheme.colors.primary),
+ ) {}
+
+ Spacer(GlanceModifier.width(8.dp))
+
+ // event details
+ Column(modifier = GlanceModifier.defaultWeight()) {
+ Text(
+ text = event.title,
+ style = TextStyle(
+ color = GlanceTheme.colors.onSurface,
+ fontSize = 13.sp,
+ fontWeight = FontWeight.Medium,
+ ),
+ maxLines = 2,
+ )
+ if (event.location != null) {
+ Text(
+ text = event.location,
+ style = TextStyle(
+ color = GlanceTheme.colors.onSurfaceVariant,
+ fontSize = 11.sp,
+ ),
+ maxLines = 1,
+ )
+ }
+ }
+ }
+ }
+
+ companion object {
+ private val timeFormatter = DateTimeFormatter.ofPattern("HH:mm")
+ }
+}
+
+private sealed class AgendaListItem {
+ data class Header(val date: LocalDate) : AgendaListItem()
+ data class Event(val event: AgendaEvent) : AgendaListItem()
+}
+
+class AgendaWidgetReceiver : GlanceAppWidgetReceiver() {
+ override val glanceAppWidget: GlanceAppWidget = AgendaWidget()
+}
diff --git a/app/src/main/res/layout/glance_default_loading_layout.xml b/app/src/main/res/layout/glance_default_loading_layout.xml
new file mode 100644
index 0000000..458d10b
--- /dev/null
+++ b/app/src/main/res/layout/glance_default_loading_layout.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
index c162273..fa58d20 100644
--- a/app/src/main/res/values-night/themes.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -1,16 +1,3 @@
-
-
-
-
\ No newline at end of file
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 7aeadb5..b70a0ee 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,5 @@
- Nova Shell for Android
-
\ No newline at end of file
+ Nova Shell
+ Calendar agenda showing upcoming events
+ Loading…
+
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 967ee9d..57674aa 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,16 +1,6 @@
-
-
-
-
\ No newline at end of file
+
diff --git a/app/src/main/res/xml/agenda_widget_info.xml b/app/src/main/res/xml/agenda_widget_info.xml
new file mode 100644
index 0000000..963cab5
--- /dev/null
+++ b/app/src/main/res/xml/agenda_widget_info.xml
@@ -0,0 +1,15 @@
+
+
diff --git a/build.gradle.kts b/build.gradle.kts
index 3756278..b93419d 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,4 +1,5 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
-}
\ No newline at end of file
+ alias(libs.plugins.kotlin.android) apply false
+ alias(libs.plugins.compose.compiler) apply false
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 13d6f4e..70570a4 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,20 +1,33 @@
[versions]
agp = "9.2.0"
+kotlin = "2.1.20"
coreKtx = "1.18.0"
+lifecycleRuntime = "2.9.0"
+activityCompose = "1.10.1"
+composeBom = "2025.04.01"
+glance = "1.1.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
-appcompat = "1.6.1"
-material = "1.13.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntime" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
+androidx-material-icons = { group = "androidx.compose.material", name = "material-icons-extended" }
+androidx-glance = { group = "androidx.glance", name = "glance-appwidget", version.ref = "glance" }
+androidx-glance-m3 = { group = "androidx.glance", name = "glance-material3", version.ref = "glance" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
-androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
-material = { group = "com.google.android.material", name = "material", version.ref = "material" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
-
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }