From d54a7ef5eb6ac764ae6e1898c78f95caa174c732 Mon Sep 17 00:00:00 2001 From: Damocles Date: Wed, 22 Apr 2026 20:34:02 +0200 Subject: [PATCH] add material you calendar agenda widget with compose + glance --- app/build.gradle.kts | 44 ++- app/src/main/AndroidManifest.xml | 27 +- .../darkest/nova/android/MainActivity.kt | 47 ++++ .../nova/android/data/CalendarRepository.kt | 90 +++++++ .../darkest/nova/android/ui/AgendaScreen.kt | 199 ++++++++++++++ .../darkest/nova/android/ui/theme/Theme.kt | 26 ++ .../nova/android/widget/AgendaWidget.kt | 254 ++++++++++++++++++ .../layout/glance_default_loading_layout.xml | 12 + app/src/main/res/values-night/themes.xml | 19 +- app/src/main/res/values/strings.xml | 6 +- app/src/main/res/values/themes.xml | 20 +- app/src/main/res/xml/agenda_widget_info.xml | 15 ++ build.gradle.kts | 5 +- gradle/libs.versions.toml | 23 +- 14 files changed, 733 insertions(+), 54 deletions(-) create mode 100644 app/src/main/java/space/darkest/nova/android/MainActivity.kt create mode 100644 app/src/main/java/space/darkest/nova/android/data/CalendarRepository.kt create mode 100644 app/src/main/java/space/darkest/nova/android/ui/AgendaScreen.kt create mode 100644 app/src/main/java/space/darkest/nova/android/ui/theme/Theme.kt create mode 100644 app/src/main/java/space/darkest/nova/android/widget/AgendaWidget.kt create mode 100644 app/src/main/res/layout/glance_default_loading_layout.xml create mode 100644 app/src/main/res/xml/agenda_widget_info.xml 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 + + - \ 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" }