add material you calendar agenda widget with compose + glance

This commit is contained in:
Damocles 2026-04-22 20:34:02 +02:00
parent db4c6a7f63
commit d54a7ef5eb
14 changed files with 733 additions and 54 deletions

View file

@ -1,14 +1,12 @@
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.compose.compiler)
} }
android { android {
namespace = "space.darkest.nova.android" namespace = "space.darkest.nova.android"
compileSdk { compileSdk = 36
version = release(36) {
minorApiLevel = 1
}
}
defaultConfig { defaultConfig {
applicationId = "space.darkest.nova.android" applicationId = "space.darkest.nova.android"
@ -22,24 +20,46 @@ android {
buildTypes { buildTypes {
release { release {
isMinifyEnabled = false isMinifyEnabled = true
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro"
) )
} }
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
} }
} }
dependencies { dependencies {
implementation(libs.androidx.appcompat)
implementation(libs.androidx.core.ktx) 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) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
} androidTestImplementation(libs.androidx.espresso.core)
}

View file

@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.READ_CALENDAR" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
@ -10,6 +12,27 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.NovaShellForAndroid" /> android:theme="@style/Theme.NovaShellForAndroid">
</manifest> <activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.NovaShellForAndroid">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver
android:name=".widget.AgendaWidgetReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/agenda_widget_info" />
</receiver>
</application>
</manifest>

View file

@ -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<space.darkest.nova.android.data.AgendaDay>())
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)
}
}

View file

@ -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<AgendaEvent>,
)
object CalendarRepository {
fun getAgenda(context: Context, days: Int = 14): List<AgendaDay> {
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<AgendaEvent>()
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 }
}
}

View file

@ -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<AgendaDay>) {
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")

View file

@ -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,
)
}

View file

@ -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<AgendaDay>) {
Scaffold(
titleBar = TitleBar(
startIcon = null,
title = "Agenda",
textColor = GlanceTheme.colors.onSurface,
),
backgroundColor = GlanceTheme.colors.widgetBackground,
modifier = GlanceModifier.clickable(actionStartActivity<MainActivity>()),
) {
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<AgendaDay>) {
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()
}

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/widget_loading"
android:textAppearance="@android:style/TextAppearance.Material.Body1" />
</FrameLayout>

View file

@ -1,16 +1,3 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<!-- Base application theme. --> <style name="Theme.NovaShellForAndroid" parent="Theme.Material3.DynamicColors.DayNight" />
<style name="Theme.NovaShellForAndroid" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> </resources>
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View file

@ -1,3 +1,5 @@
<resources> <resources>
<string name="app_name">Nova Shell for Android</string> <string name="app_name">Nova Shell</string>
</resources> <string name="widget_description">Calendar agenda showing upcoming events</string>
<string name="widget_loading">Loading…</string>
</resources>

View file

@ -1,16 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<!-- Base application theme. --> <style name="Theme.NovaShellForAndroid" parent="Theme.Material3.DynamicColors.DayNight">
<style name="Theme.NovaShellForAndroid" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> <item name="android:windowTranslucentStatus">false</item>
<!-- Primary brand color. --> <item name="android:windowTranslucentNavigation">false</item>
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style> </style>
</resources> </resources>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/widget_description"
android:initialLayout="@layout/glance_default_loading_layout"
android:minWidth="180dp"
android:minHeight="180dp"
android:minResizeWidth="110dp"
android:minResizeHeight="110dp"
android:maxResizeWidth="530dp"
android:maxResizeHeight="530dp"
android:resizeMode="horizontal|vertical"
android:targetCellWidth="3"
android:targetCellHeight="3"
android:updatePeriodMillis="1800000"
android:widgetCategory="home_screen" />

View file

@ -1,4 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
} alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.compose.compiler) apply false
}

View file

@ -1,20 +1,33 @@
[versions] [versions]
agp = "9.2.0" agp = "9.2.0"
kotlin = "2.1.20"
coreKtx = "1.18.0" coreKtx = "1.18.0"
lifecycleRuntime = "2.9.0"
activityCompose = "1.10.1"
composeBom = "2025.04.01"
glance = "1.1.1"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.1.5" junitVersion = "1.1.5"
espressoCore = "3.5.1" espressoCore = "3.5.1"
appcompat = "1.6.1"
material = "1.13.0"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } 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" } junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } 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-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] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } 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" }