Quick add widget: 1×1 shaped + opening the editor on today (#435)

This commit is contained in:
2026-10-23 18:51:49 +02:00
parent edf80fe8b8
commit 85d4dc535c
9 changed files with 156 additions and 0 deletions
+12
View File
@@ -352,6 +352,18 @@
android:resource="@xml/appwidget_info_month" />
</receiver>
<receiver
android:name=".widget.quickadd.QuickAddWidgetReceiver"
android:label="@string/widget_quick_add_label"
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/appwidget_info_quick_add" />
</receiver>
<!-- Keeps both widgets fresh: the calendar provider broadcasts
PROVIDER_CHANGED on any data change (external sync; the app's own
writes redraw directly). The day boundary arrives as the app's own
@@ -256,3 +256,7 @@ private suspend fun Context.queryMonthSource(
months = months,
)
}
/** Today and the app theme, for a widget that draws no events. */
internal suspend fun Context.loadToday(): Themed<LocalDate> =
Themed(today(systemZone()), widgetEntryPoint().settingsPrefs().widgetSettings.first().themeMode)
@@ -10,6 +10,8 @@ import de.jeanlucmakiola.calendula.widget.agenda.AgendaWidget
import de.jeanlucmakiola.calendula.widget.agenda.AgendaWidgetReceiver
import de.jeanlucmakiola.calendula.widget.month.MonthWidget
import de.jeanlucmakiola.calendula.widget.month.MonthWidgetReceiver
import de.jeanlucmakiola.calendula.widget.quickadd.QuickAddWidget
import de.jeanlucmakiola.calendula.widget.quickadd.QuickAddWidgetReceiver
/** A content option a widget's config screen offers, besides background and theme. */
enum class WidgetOption { RANGE, TEXT_SIZE, PAST_DISPLAY, SHOW_TODAY, CALENDARS }
@@ -37,11 +39,18 @@ enum class WidgetKind(
setOf(WidgetOption.CALENDARS),
DpSize(320.dp, 340.dp),
),
QUICK_ADD(
QuickAddWidgetReceiver::class.java,
R.string.widget_quick_add_label,
emptySet(),
DpSize(76.dp, 76.dp),
),
;
fun widget(): GlanceAppWidget = when (this) {
AGENDA -> AgendaWidget()
MONTH -> MonthWidget()
QUICK_ADD -> QuickAddWidget()
}
/** Whether this kind stores [AgendaWidgetOptions] of its own. */
@@ -48,6 +48,7 @@ import de.jeanlucmakiola.calendula.widget.WidgetTheme
import de.jeanlucmakiola.calendula.widget.agenda.AgendaWidgetContent
import de.jeanlucmakiola.calendula.widget.agenda.metricsFor
import de.jeanlucmakiola.calendula.widget.month.MonthWidgetContent
import de.jeanlucmakiola.calendula.widget.quickadd.QuickAddWidgetContent
import de.jeanlucmakiola.floret.components.GroupedChoiceRow
import de.jeanlucmakiola.floret.components.GroupedHint
import de.jeanlucmakiola.floret.components.GroupedRow
@@ -146,6 +147,8 @@ internal fun WidgetConfigScreen(
AgendaWidgetContent(preview.data, state.appearance, state.appTheme, preview = true)
is WidgetPreviewData.Month ->
MonthWidgetContent(preview.source, preview.index, state.appearance, state.appTheme)
is WidgetPreviewData.Today ->
QuickAddWidgetContent(preview.today, state.appearance, state.appTheme)
}
}
}
@@ -50,12 +50,14 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.LocalDate
import java.util.Locale
/** What the preview draws, loaded with the draft's options. */
sealed interface WidgetPreviewData {
data class Agenda(val data: AgendaWidgetData) : WidgetPreviewData
data class Month(val source: MonthWidgetSource, val index: Int) : WidgetPreviewData
data class Today(val today: LocalDate) : WidgetPreviewData
}
/** The options being edited, applied to the preview as they change and written on save. */
@@ -226,6 +228,7 @@ class WidgetConfigViewModel @Inject constructor(
val source = context.loadThemedMonthSource(monthWindowCenter(index), draft.hiddenCalendars)
WidgetPreviewData.Month(source.content, index)
}
WidgetKind.QUICK_ADD -> WidgetPreviewData.Today(today(systemZone()))
}
}
@@ -0,0 +1,84 @@
package de.jeanlucmakiola.calendula.widget.quickadd
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import androidx.datastore.preferences.core.Preferences
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.GlanceTheme
import androidx.glance.ImageProvider
import androidx.glance.LocalContext
import androidx.glance.LocalSize
import androidx.glance.action.clickable
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.SizeMode
import androidx.glance.appwidget.action.actionStartActivity
import androidx.glance.appwidget.provideContent
import androidx.glance.currentState
import androidx.glance.layout.Alignment
import androidx.glance.layout.Box
import androidx.glance.layout.fillMaxSize
import androidx.glance.state.PreferencesGlanceStateDefinition
import de.jeanlucmakiola.calendula.MainActivity
import de.jeanlucmakiola.calendula.R
import de.jeanlucmakiola.calendula.widget.CalendulaWidgetRoot
import de.jeanlucmakiola.calendula.widget.WidgetAppearance
import de.jeanlucmakiola.calendula.widget.CalendulaWidgetReceiver
import de.jeanlucmakiola.calendula.widget.loadToday
import de.jeanlucmakiola.calendula.widget.widgetAppearance
import de.jeanlucmakiola.floret.glance.WidgetData
import de.jeanlucmakiola.floret.glance.WidgetShape
import de.jeanlucmakiola.floret.glance.WidgetShapedIconButton
import de.jeanlucmakiola.floret.glance.currentRefreshStamp
import de.jeanlucmakiola.floret.glance.refreshStamp
import de.jeanlucmakiola.floret.prefs.ThemeMode
import kotlinx.datetime.LocalDate
/**
* 1×1 quick add (#435): one shaped "+" that opens the new-event editor on
* today, with the same defaults as the app's own new-event button.
*/
class QuickAddWidget : GlanceAppWidget() {
override val stateDefinition = PreferencesGlanceStateDefinition
// Exact: the shape fills whatever square the launcher gives it.
override val sizeMode = SizeMode.Exact
override suspend fun provideGlance(context: Context, id: GlanceId) {
val widgetData = WidgetData(context.loadToday(), refreshStamp(context, id))
provideContent {
// The rollover moves the stamp, so the editor opens on the new day.
val loaded = widgetData.current(currentRefreshStamp()) { context.loadToday() }
QuickAddWidgetContent(loaded.content, currentState<Preferences>().widgetAppearance, loaded.appTheme)
}
}
}
class QuickAddWidgetReceiver : CalendulaWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = QuickAddWidget()
}
@Composable
internal fun QuickAddWidgetContent(today: LocalDate, appearance: WidgetAppearance, appTheme: ThemeMode) {
CalendulaWidgetRoot(appearance, appTheme) {
val context = LocalContext.current
val size = LocalSize.current
val action = actionStartActivity(MainActivity.openCreateIntent(context, today))
Box(
modifier = GlanceModifier.fillMaxSize().clickable(action),
contentAlignment = Alignment.Center,
) {
WidgetShapedIconButton(
shape = WidgetShape.SUNNY,
icon = ImageProvider(R.drawable.ic_widget_add),
contentDescription = context.getString(R.string.widget_new_event),
action = action,
color = GlanceTheme.colors.primary,
iconTint = GlanceTheme.colors.onPrimary,
size = (minOf(size.width, size.height) - 12.dp).coerceIn(36.dp, 120.dp),
)
}
}
}
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Static mock shown in the widget picker (android:previewLayout). -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/preview_widget_bg"
android:padding="6dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAccessibility="no"
android:src="@drawable/floret_widget_shape_sunny"
android:tint="@color/widget_preview_primary"
tools:ignore="UseAppTint" />
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:importantForAccessibility="no"
android:src="@drawable/ic_widget_add"
android:tint="@color/widget_preview_on_primary"
tools:ignore="UseAppTint" />
</FrameLayout>
+1
View File
@@ -435,6 +435,7 @@
<string name="widget_agenda_title">Upcoming</string>
<string name="widget_agenda_label">Calendula agenda</string>
<string name="widget_month_label">Calendula month</string>
<string name="widget_quick_add_label">Calendula quick add</string>
<string name="widget_refresh">Refresh</string>
<string name="widget_new_event">New event</string>
<string name="widget_needs_permission">Open Calendula to grant calendar access</string>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="40dp"
android:minHeight="40dp"
android:targetCellWidth="1"
android:targetCellHeight="1"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen"
android:configure="de.jeanlucmakiola.calendula.widget.config.WidgetConfigActivity"
android:widgetFeatures="reconfigurable"
android:description="@string/widget_quick_add_label"
android:updatePeriodMillis="1800000"
android:previewLayout="@layout/widget_preview_quick_add"
android:initialLayout="@layout/glance_default_loading_layout" />