diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c1e42a6..9789b41 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -140,6 +140,18 @@ android:resource="@xml/widget_agenda_info" /> + + + + + + + = PreferencesGlanceStateDefinition + + override suspend fun provideGlance(context: Context, id: GlanceId) { + val localized = LocaleHelper.wrap(context) + val colors = WidgetTheme.colors(context) + val weekModel = WidgetDataLoader.loadAgenda(context, AgendaRange.WEEK) + val monthModel = WidgetDataLoader.loadAgenda(context, AgendaRange.MONTH) + val openCalendar = actionStartActivity(WidgetIntents.openTab(context, "calendar")) + provideContent { + val range = rangeFromPrefs(currentState()) + val model = if (range == AgendaRange.MONTH) monthModel else weekModel + GlanceTheme { + LockAgendaContent( + context = localized, + colors = colors, + model = model, + openCalendar = openCalendar, + ) + } + } + } + + companion object { + val RangePrefKey = stringPreferencesKey("lock_agenda_range") + const val MaxEvents = 4 + + fun rangeFromPrefs(prefs: Preferences): AgendaRange = + if (prefs[RangePrefKey] == "month") AgendaRange.MONTH else AgendaRange.WEEK + } +} + +@Composable +private fun LockAgendaContent( + context: Context, + colors: WidgetColors, + model: AgendaWidgetModel, + openCalendar: androidx.glance.action.Action, +) { + val events = model.events.take(LockAgendaCalendarWidget.MaxEvents) + Column( + modifier = GlanceModifier + .fillMaxSize() + .cornerRadius(16.dp) + .background(colors.surface) + .padding(horizontal = 10.dp, vertical = 8.dp) + .clickable(openCalendar), + ) { + Row( + modifier = GlanceModifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = GlanceModifier.defaultWeight()) { + Text( + text = model.headerTitle, + style = TextStyle( + color = colors.primary, + fontSize = 13.sp, + fontWeight = FontWeight.Bold, + ), + maxLines = 1, + ) + Text( + text = model.subtitle, + style = TextStyle( + color = colors.onSurfaceVariant, + fontSize = 10.sp, + ), + maxLines = 1, + ) + } + Text( + text = "W", + style = TextStyle( + color = if (model.range == AgendaRange.WEEK) colors.primary else colors.onSurfaceVariant, + fontSize = 10.sp, + fontWeight = FontWeight.Medium, + ), + ) + Spacer(modifier = GlanceModifier.width(2.dp)) + Switch( + checked = model.range == AgendaRange.MONTH, + onCheckedChange = actionRunCallback(), + colors = SwitchDefaults.switchColors( + checkedThumbColor = colors.onPrimary, + checkedTrackColor = colors.primary, + uncheckedThumbColor = colors.onSurfaceVariant, + uncheckedTrackColor = colors.primaryContainer, + ), + ) + Spacer(modifier = GlanceModifier.width(2.dp)) + Text( + text = "M", + style = TextStyle( + color = if (model.range == AgendaRange.MONTH) colors.primary else colors.onSurfaceVariant, + fontSize = 10.sp, + fontWeight = FontWeight.Medium, + ), + ) + } + Spacer(modifier = GlanceModifier.height(6.dp)) + if (events.isEmpty()) { + Text( + text = if (model.range == AgendaRange.WEEK) { + context.getString(app.taskdav.R.string.widget_empty_events_week) + } else { + context.getString(app.taskdav.R.string.widget_empty_events_month) + }, + style = TextStyle(color = colors.onSurfaceVariant, fontSize = 11.sp), + maxLines = 2, + ) + } else { + LazyColumn(modifier = GlanceModifier.fillMaxSize()) { + items(events, itemId = { it.id }) { event -> + Row( + modifier = GlanceModifier + .fillMaxWidth() + .padding(vertical = 2.dp), + ) { + Text( + text = event.timeLabel, + style = TextStyle( + color = colors.onSurfaceVariant, + fontSize = 10.sp, + fontWeight = FontWeight.Medium, + ), + maxLines = 1, + modifier = GlanceModifier.width(72.dp), + ) + Spacer(modifier = GlanceModifier.width(4.dp)) + Text( + text = event.title, + style = TextStyle(color = colors.onSurface, fontSize = 12.sp), + maxLines = 1, + modifier = GlanceModifier.defaultWeight(), + ) + } + } + } + } + } +} + +class ToggleLockAgendaRangeAction : ActionCallback { + override suspend fun onAction( + context: Context, + glanceId: GlanceId, + parameters: ActionParameters, + ) { + val toMonth = parameters[ToggleableStateKey] == true + val value = if (toMonth) "month" else "week" + Log.i("LockAgendaWidget", "Toggle range → $value") + updateAppWidgetState(context, glanceId) { prefs -> + prefs[LockAgendaCalendarWidget.RangePrefKey] = value + } + LockAgendaCalendarWidget().update(context, glanceId) + } +} + +class LockAgendaCalendarWidgetReceiver : GlanceAppWidgetReceiver() { + override val glanceAppWidget: GlanceAppWidget = LockAgendaCalendarWidget() +} diff --git a/app/src/main/java/app/taskdav/widget/WidgetDataLoader.kt b/app/src/main/java/app/taskdav/widget/WidgetDataLoader.kt index 440cd5a..cacd95d 100644 --- a/app/src/main/java/app/taskdav/widget/WidgetDataLoader.kt +++ b/app/src/main/java/app/taskdav/widget/WidgetDataLoader.kt @@ -3,6 +3,7 @@ package app.taskdav.widget import android.content.Context import app.taskdav.R import app.taskdav.TaskDavApp +import app.taskdav.data.EventEntity import app.taskdav.data.LocaleHelper import app.taskdav.domain.EventRecurrence import app.taskdav.domain.TaskTreeBuilder @@ -97,30 +98,28 @@ object WidgetDataLoader { val loc = locale(context) val app = context.applicationContext as TaskDavApp val now = System.currentTimeMillis() - val (rangeStart, rangeEnd, header, subtitle) = when (range) { + val (rangeEnd, header, subtitle) = when (range) { AgendaRange.WEEK -> { - val start = CalendarViewModel.startOfWeek(now) - val end = start + TimeUnit.DAYS.toMillis(7) + val weekStart = CalendarViewModel.startOfWeek(now) + val end = weekStart + TimeUnit.DAYS.toMillis(7) val endLabel = SimpleDateFormat("MMM d", loc) .format(Date(end - TimeUnit.DAYS.toMillis(1))) - val startLabel = SimpleDateFormat("MMM d", loc).format(Date(start)) + val startLabel = SimpleDateFormat("MMM d", loc).format(Date(now)) AgendaBounds( - start, end, strings.getString(R.string.widget_subtitle_this_week), "$startLabel – $endLabel", ) } AgendaRange.MONTH -> { - val start = CalendarViewModel.startOfMonth(now) + val monthStart = CalendarViewModel.startOfMonth(now) val cal = Calendar.getInstance().apply { - timeInMillis = start + timeInMillis = monthStart add(Calendar.MONTH, 1) } val end = cal.timeInMillis - val monthLabel = SimpleDateFormat("MMMM yyyy", loc).format(Date(start)) + val monthLabel = SimpleDateFormat("MMMM yyyy", loc).format(Date(monthStart)) AgendaBounds( - start, end, monthLabel, strings.getString(R.string.widget_subtitle_this_month), @@ -128,7 +127,9 @@ object WidgetDataLoader { } } val events = app.database.events().getActive() - val expanded = EventRecurrence.expandAll(events, rangeStart, rangeEnd) + // From now forward only — skip earlier days this week/month and events that already ended. + val expanded = EventRecurrence.expandAll(events, now, rangeEnd) + .filter { event -> eventStillRelevant(event, now) } .sortedBy { it.dtStartMillis ?: Long.MAX_VALUE } val dayFmt = SimpleDateFormat("EEE d", loc) return AgendaWidgetModel( @@ -154,6 +155,18 @@ object WidgetDataLoader { ) } + /** True while the occurrence has not finished (ongoing or future). */ + private fun eventStillRelevant(event: EventEntity, now: Long): Boolean { + val start = event.dtStartMillis ?: return false + val end = event.dtEndMillis?.takeIf { it > start } + ?: if (event.allDay) { + start + TimeUnit.DAYS.toMillis(1) + } else { + start + TimeUnit.HOURS.toMillis(1) + } + return end > now + } + suspend fun loadTasks(context: Context): TasksWidgetModel { val strings = localized(context) val app = context.applicationContext as TaskDavApp @@ -212,7 +225,6 @@ object WidgetDataLoader { } private data class AgendaBounds( - val start: Long, val end: Long, val header: String, val subtitle: String, diff --git a/app/src/main/java/app/taskdav/widget/WidgetUpdater.kt b/app/src/main/java/app/taskdav/widget/WidgetUpdater.kt index 57b1ae9..c92e50b 100644 --- a/app/src/main/java/app/taskdav/widget/WidgetUpdater.kt +++ b/app/src/main/java/app/taskdav/widget/WidgetUpdater.kt @@ -25,6 +25,7 @@ object WidgetUpdater { withContext(Dispatchers.Default) { CalendarWidget().updateAll(appContext) AgendaCalendarWidget().updateAll(appContext) + LockAgendaCalendarWidget().updateAll(appContext) TasksWidget().updateAll(appContext) NotesWidget().updateAll(appContext) } diff --git a/app/src/main/res/layout/widget_lock_agenda_preview.xml b/app/src/main/res/layout/widget_lock_agenda_preview.xml new file mode 100644 index 0000000..668b1a5 --- /dev/null +++ b/app/src/main/res/layout/widget_lock_agenda_preview.xml @@ -0,0 +1,29 @@ + + + + + + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index d478671..887d164 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -80,10 +80,12 @@ TaskDav Eventos del calendario de hoy Eventos de esta semana o mes + Agenda compacta de semana o mes para la pantalla de bloqueo Tareas abiertas con fecha de vencimiento Notas recientes TaskDav Hoy TaskDav Semana / Mes + TaskDav Agenda bloqueo TaskDav Tareas TaskDav Notas diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 531b14e..421678f 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -80,10 +80,12 @@ TaskDav Eventi del calendario di oggi Eventi di questa settimana o mese + Agenda compatta settimana o mese per la schermata di blocco Attività aperte con scadenza Note recenti TaskDav Oggi TaskDav Settimana / Mese + TaskDav Agenda blocco TaskDav Attività TaskDav Note diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9c24130..15aabf9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -80,10 +80,12 @@ TaskDav Today\'s calendar events This week or month\'s events + Compact week or month agenda for the lock screen Open tasks with due dates Recent notes TaskDav Today TaskDav Week / Month + TaskDav Lock agenda TaskDav Tasks TaskDav Notes diff --git a/app/src/main/res/xml/widget_agenda_info.xml b/app/src/main/res/xml/widget_agenda_info.xml index aa11e8a..71b3150 100644 --- a/app/src/main/res/xml/widget_agenda_info.xml +++ b/app/src/main/res/xml/widget_agenda_info.xml @@ -8,5 +8,5 @@ android:initialLayout="@layout/widget_loading" android:previewLayout="@layout/widget_agenda_preview" android:resizeMode="horizontal|vertical" - android:widgetCategory="home_screen" + android:widgetCategory="home_screen|keyguard" android:description="@string/widget_agenda_description" /> diff --git a/app/src/main/res/xml/widget_lock_agenda_info.xml b/app/src/main/res/xml/widget_lock_agenda_info.xml new file mode 100644 index 0000000..53f7e1b --- /dev/null +++ b/app/src/main/res/xml/widget_lock_agenda_info.xml @@ -0,0 +1,14 @@ + +