add clock widget with dual-color time and next alarm

This commit is contained in:
Damocles 2026-04-22 22:00:22 +02:00
parent 1dbd96d961
commit 2335e6d926
4 changed files with 183 additions and 0 deletions

View file

@ -35,5 +35,16 @@
android:name="android.appwidget.provider"
android:resource="@xml/agenda_widget_info" />
</receiver>
<receiver
android:name=".widget.ClockWidgetReceiver"
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/clock_widget_info" />
</receiver>
</application>
</manifest>

View file

@ -0,0 +1,156 @@
package space.darkest.nova.android.widget
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
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.GlanceAppWidgetReceiver
import androidx.glance.appwidget.action.actionStartActivity
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.height
import androidx.glance.layout.padding
import androidx.glance.layout.width
import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import java.time.Instant
import java.time.LocalDate
import java.time.LocalTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.TextStyle as DateTextStyle
import java.util.Locale
class ClockWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
val alarmManager = context.getSystemService(AlarmManager::class.java)
val nextAlarm = alarmManager.nextAlarmClock?.let { alarm ->
Instant.ofEpochMilli(alarm.triggerTime)
.atZone(ZoneId.systemDefault())
.toLocalDateTime()
}
provideContent {
GlanceTheme(colors = DynamicThemeColorProviders) {
ClockContent(nextAlarm?.toLocalTime(), nextAlarm?.toLocalDate())
}
}
}
@Composable
private fun ClockContent(alarmTime: LocalTime?, alarmDate: LocalDate?) {
val now = LocalTime.now()
val today = LocalDate.now()
val hours = now.format(DateTimeFormatter.ofPattern("HH"))
val minutes = now.format(DateTimeFormatter.ofPattern("mm"))
val dateStr = today.let {
val day = it.dayOfWeek.getDisplayName(DateTextStyle.SHORT, Locale.getDefault())
val date = it.format(DateTimeFormatter.ofPattern("d MMM"))
"$day, $date"
}
Box(
modifier = GlanceModifier
.fillMaxSize()
.background(GlanceTheme.colors.widgetBackground)
.padding(16.dp)
.clickable(actionStartActivity(Intent(AlarmClock.ACTION_SHOW_ALARMS))),
contentAlignment = Alignment.CenterStart,
) {
Column {
// time row: hours (primary) : minutes (tertiary)
Row(verticalAlignment = Alignment.Bottom) {
Text(
text = hours,
style = TextStyle(
color = GlanceTheme.colors.primary,
fontSize = 48.sp,
fontWeight = FontWeight.Bold,
),
)
Text(
text = ":",
style = TextStyle(
color = GlanceTheme.colors.onSurfaceVariant,
fontSize = 48.sp,
fontWeight = FontWeight.Bold,
),
)
Text(
text = minutes,
style = TextStyle(
color = GlanceTheme.colors.tertiary,
fontSize = 48.sp,
fontWeight = FontWeight.Bold,
),
)
}
Spacer(GlanceModifier.height(2.dp))
// date
Text(
text = dateStr,
style = TextStyle(
color = GlanceTheme.colors.onSurfaceVariant,
fontSize = 14.sp,
),
)
// next alarm
if (alarmTime != null && alarmDate != null) {
Spacer(GlanceModifier.height(4.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "\u23F0",
style = TextStyle(fontSize = 12.sp),
)
Spacer(GlanceModifier.width(4.dp))
Text(
text = formatAlarm(alarmTime, alarmDate),
style = TextStyle(
color = GlanceTheme.colors.secondary,
fontSize = 12.sp,
),
)
}
}
}
}
}
private fun formatAlarm(time: LocalTime, date: LocalDate): String {
val today = LocalDate.now()
val timeStr = time.format(DateTimeFormatter.ofPattern("HH:mm"))
return when (date) {
today -> timeStr
today.plusDays(1) -> "Tomorrow $timeStr"
else -> {
val dayName = date.dayOfWeek.getDisplayName(DateTextStyle.SHORT, Locale.getDefault())
"$dayName $timeStr"
}
}
}
}
class ClockWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = ClockWidget()
}

View file

@ -1,5 +1,6 @@
<resources>
<string name="app_name">Nova Shell</string>
<string name="widget_description">Calendar agenda showing upcoming events</string>
<string name="clock_widget_description">Clock with next alarm</string>
<string name="widget_loading">Loading…</string>
</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/clock_widget_description"
android:initialLayout="@layout/glance_default_loading_layout"
android:minWidth="180dp"
android:minHeight="110dp"
android:minResizeWidth="110dp"
android:minResizeHeight="80dp"
android:maxResizeWidth="530dp"
android:maxResizeHeight="200dp"
android:resizeMode="horizontal|vertical"
android:targetCellWidth="3"
android:targetCellHeight="2"
android:updatePeriodMillis="60000"
android:widgetCategory="home_screen" />