initial commit: material you calendar widget

This commit is contained in:
Damocles 2026-04-22 01:06:53 +02:00
commit be5362ad76
19 changed files with 754 additions and 0 deletions

41
app/build.gradle.kts Normal file
View file

@ -0,0 +1,41 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "dev.nova.widget"
compileSdk = 35
buildToolsVersion = "35.0.0"
defaultConfig {
applicationId = "dev.nova.widget"
minSdk = 31 // Material You requires Android 12+
targetSdk = 35
versionCode = 1
versionName = "0.1.0"
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
kotlinOptions {
jvmTarget = "21"
}
}
dependencies {
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.core:core-ktx:1.15.0")
implementation("androidx.glance:glance-appwidget:1.1.1")
implementation("androidx.glance:glance-material3:1.1.1")
}

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="Nova Calendar"
android:supportsRtl="true"
android:theme="@style/Theme.NovaWidget">
<receiver
android:name=".CalendarWidgetReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/calendar_widget_info" />
</receiver>
</application>
</manifest>

View file

@ -0,0 +1,157 @@
package dev.nova.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.clickable
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.action.actionStartActivity
import androidx.glance.appwidget.provideContent
import androidx.glance.background
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.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextAlign
import androidx.glance.text.TextStyle
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.YearMonth
import java.time.format.TextStyle as DateTextStyle
import java.util.Locale
class CalendarWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
GlanceTheme {
CalendarContent()
}
}
}
@Composable
private fun CalendarContent() {
val today = LocalDate.now()
val yearMonth = YearMonth.from(today)
val daysInMonth = yearMonth.lengthOfMonth()
val firstDayOfWeek = yearMonth.atDay(1).dayOfWeek
// Monday-based offset
val startOffset = (firstDayOfWeek.value - DayOfWeek.MONDAY.value + 7) % 7
val calendarIntent = Intent(Intent.ACTION_VIEW).apply {
data = CalendarContract.CONTENT_URI
}
Column(
modifier = GlanceModifier
.fillMaxSize()
.background(GlanceTheme.colors.widgetBackground)
.padding(12.dp)
.clickable(actionStartActivity(calendarIntent)),
) {
// Header - month and year
Text(
text = "${yearMonth.month.getDisplayName(DateTextStyle.FULL, Locale.getDefault())} ${yearMonth.year}",
style = TextStyle(
color = GlanceTheme.colors.primary,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
),
modifier = GlanceModifier.fillMaxWidth().padding(bottom = 8.dp),
)
// Day of week headers
Row(modifier = GlanceModifier.fillMaxWidth()) {
val days = listOf("Mo", "Tu", "We", "Th", "Fr", "Sa", "Su")
for (day in days) {
Text(
text = day,
style = TextStyle(
color = GlanceTheme.colors.secondary,
fontSize = 11.sp,
fontWeight = FontWeight.Medium,
textAlign = TextAlign.Center,
),
modifier = GlanceModifier.defaultWeight(),
)
}
}
Spacer(modifier = GlanceModifier.height(4.dp))
// Calendar grid
var dayCounter = 1
for (week in 0 until 6) {
if (dayCounter > daysInMonth) break
Row(modifier = GlanceModifier.fillMaxWidth()) {
for (col in 0 until 7) {
val cellIndex = week * 7 + col
if (cellIndex < startOffset || dayCounter > daysInMonth) {
Spacer(modifier = GlanceModifier.defaultWeight())
} else {
val isToday = dayCounter == today.dayOfMonth
DayCell(
day = dayCounter,
isToday = isToday,
modifier = GlanceModifier.defaultWeight(),
)
dayCounter++
}
}
}
}
}
}
@Composable
private fun DayCell(day: Int, isToday: Boolean, modifier: GlanceModifier) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier.padding(vertical = 2.dp),
) {
if (isToday) {
Box(
contentAlignment = Alignment.Center,
modifier = GlanceModifier
.size(28.dp)
.background(GlanceTheme.colors.primary),
) {
Text(
text = day.toString(),
style = TextStyle(
color = GlanceTheme.colors.onPrimary,
fontSize = 13.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
),
)
}
} else {
Text(
text = day.toString(),
style = TextStyle(
color = GlanceTheme.colors.onSurface,
fontSize = 13.sp,
textAlign = TextAlign.Center,
),
)
}
}
}
}

View file

@ -0,0 +1,8 @@
package dev.nova.widget
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
class CalendarWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = CalendarWidget()
}

View file

@ -0,0 +1,15 @@
<?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"
android:background="@android:color/system_accent1_100"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Loading..."
android:textColor="@android:color/system_accent1_900" />
</FrameLayout>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="widget_background">@android:color/system_accent1_900</color>
<color name="widget_on_background">@android:color/system_accent1_100</color>
<color name="widget_surface">@android:color/system_neutral1_900</color>
<color name="widget_on_surface">@android:color/system_neutral1_100</color>
<color name="widget_primary">@android:color/system_accent1_200</color>
<color name="widget_on_primary">@android:color/system_accent1_800</color>
<color name="widget_today_bg">@android:color/system_accent1_200</color>
<color name="widget_today_text">@android:color/system_accent1_800</color>
</resources>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Fallback colors when dynamic colors are unavailable -->
<color name="widget_background">@android:color/system_accent1_100</color>
<color name="widget_on_background">@android:color/system_accent1_900</color>
<color name="widget_surface">@android:color/system_neutral1_50</color>
<color name="widget_on_surface">@android:color/system_neutral1_900</color>
<color name="widget_primary">@android:color/system_accent1_600</color>
<color name="widget_on_primary">@android:color/system_accent1_0</color>
<color name="widget_today_bg">@android:color/system_accent1_600</color>
<color name="widget_today_text">@android:color/system_accent1_0</color>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.NovaWidget" parent="Theme.Material3.DynamicColors.DayNight" />
</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:initialLayout="@layout/calendar_widget_loading"
android:minWidth="250dp"
android:minHeight="250dp"
android:minResizeWidth="180dp"
android:minResizeHeight="180dp"
android:maxResizeWidth="530dp"
android:maxResizeHeight="530dp"
android:resizeMode="horizontal|vertical"
android:targetCellWidth="4"
android:targetCellHeight="4"
android:updatePeriodMillis="1800000"
android:widgetCategory="home_screen"
android:widgetFeatures="reconfigurable" />