Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6cec2f95e | ||
|
|
d012948de6 | ||
|
|
8ff8a2de1f | ||
|
|
3f81bb9ea5 | ||
|
|
a08ad15c4a | ||
|
|
c9cc40ff60 | ||
|
|
343a8904ea | ||
|
|
075940d29e | ||
|
|
670cdb226a | ||
|
|
a8da8bc7a3 | ||
|
|
cfcba283f1 | ||
|
|
85d4dc535c | ||
|
|
edf80fe8b8 | ||
|
|
0ecd6de27b |
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **Six new home-screen widgets.** Quick add (1×1) opens a new event on today with the same defaults as the app's + button ([#435]). Date (1×1) shows today's weekday and day number on a rounded shape and opens today ([#376]). Week strip (4×1) shows this week's days with a dot in each event's colour and today filled ([#436]). Upcoming (2×2) shows today's date over your next events, as many as fit ([#437]). Today lists today's events; once they're over it says "Nothing left today" and shows tomorrow's first one or two ([#438]). Countdown counts down to your next timed event, or to an event or series you pick, like a holiday in 12 days ([#439]).
|
||||
- **A month and day widget.** A compact month grid sits beside the events of one day. Tap a day in the grid to see its events, and tap the list to open that day in the app ([#441]).
|
||||
- **Each widget has its own settings.** Placing a widget, or long-pressing it and choosing Edit, opens a settings screen with a live preview. Pick a solid, tinted or transparent background, a light or dark theme or the app's, and which calendars it shows. Widgets that list events also have their time range, text size and how past events look. Settings → Widgets lists the widgets on your home screen so you can change them from there. These options used to be shared by all widgets; widgets you already placed keep what you had set ([#292]).
|
||||
|
||||
### Changed
|
||||
- **The agenda and month widgets have a new look.** The agenda puts each day in its own card, with a round date pill beside the first event and today's pill filled. The month widget has rounded day cells, weekday pills, shaded days outside the month and a + for a new event. The agenda's refresh button is gone, since widgets now update on their own ([#71], [#90]).
|
||||
- **Tapping a day in the month widget opens that day,** wherever in its cell you tap ([#386]).
|
||||
- **The widget picker shows what each widget looks like.** On Android 15 and later the previews are the real widgets with sample events, in your language and colours; on older versions they are redrawn to match.
|
||||
|
||||
### Fixed
|
||||
- **Widgets show your changes straight away.** After you added, edited or deleted an event, a widget could keep showing the old events until something else redrew it ([#420], [#229]).
|
||||
- **The month widget handles busy months.** It could show "Can't show content", or only one or two events per day. Each day now shows as many events as its height fits, and the rest fold into +N ([#372]).
|
||||
|
||||
## [2.23.0] — 2026-10-21
|
||||
|
||||
### Added
|
||||
@@ -1889,3 +1903,17 @@ automatically, with zero telemetry and no internet permission.
|
||||
[#178]: https://codeberg.org/jlmakiola/calendula/issues/178
|
||||
[#116]: https://codeberg.org/jlmakiola/calendula/issues/116
|
||||
[#413]: https://codeberg.org/jlmakiola/calendula/issues/413
|
||||
[#71]: https://codeberg.org/jlmakiola/calendula/issues/71
|
||||
[#90]: https://codeberg.org/jlmakiola/calendula/issues/90
|
||||
[#229]: https://codeberg.org/jlmakiola/calendula/issues/229
|
||||
[#292]: https://codeberg.org/jlmakiola/calendula/issues/292
|
||||
[#372]: https://codeberg.org/jlmakiola/calendula/issues/372
|
||||
[#376]: https://codeberg.org/jlmakiola/calendula/issues/376
|
||||
[#386]: https://codeberg.org/jlmakiola/calendula/issues/386
|
||||
[#420]: https://codeberg.org/jlmakiola/calendula/issues/420
|
||||
[#435]: https://codeberg.org/jlmakiola/calendula/issues/435
|
||||
[#436]: https://codeberg.org/jlmakiola/calendula/issues/436
|
||||
[#437]: https://codeberg.org/jlmakiola/calendula/issues/437
|
||||
[#438]: https://codeberg.org/jlmakiola/calendula/issues/438
|
||||
[#439]: https://codeberg.org/jlmakiola/calendula/issues/439
|
||||
[#441]: https://codeberg.org/jlmakiola/calendula/issues/441
|
||||
|
||||
@@ -32,8 +32,8 @@ android {
|
||||
// (up to 2.22.3: 2.22.3 -> 22203; from 2.22.4: 2.23.0-beta.1 -> 2230001,
|
||||
// 2.22.4 -> 2220499), and CI fails if the committed one doesn't match.
|
||||
// See docs/RELEASING.md.
|
||||
versionCode = 2230099
|
||||
versionName = "2.23.0"
|
||||
versionCode = 2240001
|
||||
versionName = "2.24.0-beta.1"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -352,6 +352,95 @@
|
||||
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>
|
||||
|
||||
<receiver
|
||||
android:name=".widget.date.DateWidgetReceiver"
|
||||
android:label="@string/widget_date_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_date" />
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".widget.week.WeekStripWidgetReceiver"
|
||||
android:label="@string/widget_week_strip_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_week_strip" />
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".widget.upcoming.UpcomingWidgetReceiver"
|
||||
android:label="@string/widget_upcoming_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_upcoming" />
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".widget.today.TodayWidgetReceiver"
|
||||
android:label="@string/widget_today_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_today" />
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".widget.countdown.CountdownWidgetReceiver"
|
||||
android:label="@string/widget_countdown_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_countdown" />
|
||||
</receiver>
|
||||
|
||||
<!-- A countdown widget's redraw for when its label changes (#439). -->
|
||||
<receiver
|
||||
android:name=".widget.countdown.CountdownTickReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<receiver
|
||||
android:name=".widget.split.MonthDayWidgetReceiver"
|
||||
android:label="@string/widget_month_day_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_month_day" />
|
||||
</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
|
||||
|
||||
@@ -10,6 +10,7 @@ import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesScheduler
|
||||
import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesSyncWorker
|
||||
import de.jeanlucmakiola.calendula.data.reminders.ReminderMaintenanceScheduler
|
||||
import de.jeanlucmakiola.calendula.data.reminders.ReminderMaintenanceWorker
|
||||
import de.jeanlucmakiola.calendula.widget.publishCalendulaWidgetPreviews
|
||||
import de.jeanlucmakiola.calendula.widget.syncWidgetRollover
|
||||
import de.jeanlucmakiola.calendula.widget.widgetEntryPoint
|
||||
import de.jeanlucmakiola.floret.crash.CrashConfig
|
||||
@@ -58,10 +59,12 @@ class CalendulaApp : Application() {
|
||||
* (#228). Idempotent, and it covers what no broadcast reaches — an alarm
|
||||
* dropped by a force-stop is armed again the next time the app is opened.
|
||||
* Off the main thread: a handful of binder calls on every process start.
|
||||
* The picker previews are republished here too, once per update.
|
||||
*/
|
||||
private fun reconcileWidgetRollover() {
|
||||
CoroutineScope(SupervisorJob() + Dispatchers.Default).launch {
|
||||
syncWidgetRollover(this@CalendulaApp)
|
||||
publishCalendulaWidgetPreviews(this@CalendulaApp)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,10 @@ import javax.inject.Singleton
|
||||
interface CalendarDataSource {
|
||||
fun calendars(): List<CalendarSource>
|
||||
fun instances(beginMillis: Long, endMillis: Long): List<EventInstance>
|
||||
|
||||
/** [instances] of event [eventId] alone, soonest first. */
|
||||
fun eventInstances(eventId: Long, beginMillis: Long, endMillis: Long): List<EventInstance> =
|
||||
instances(beginMillis, endMillis).filter { it.eventId == eventId }
|
||||
/**
|
||||
* [allDayReminderTimeMinutes]: the hour all-day reminders fire at, needed to
|
||||
* decode their stored offsets back to whole-day lead times (see
|
||||
@@ -563,21 +567,33 @@ class AndroidCalendarDataSource @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun instances(beginMillis: Long, endMillis: Long): List<EventInstance> {
|
||||
override fun instances(beginMillis: Long, endMillis: Long): List<EventInstance> =
|
||||
queryInstances(beginMillis, endMillis, null, null)
|
||||
|
||||
override fun eventInstances(eventId: Long, beginMillis: Long, endMillis: Long): List<EventInstance> =
|
||||
queryInstances(beginMillis, endMillis, "${CalendarContract.Instances.EVENT_ID} = ?", arrayOf(eventId.toString()))
|
||||
|
||||
private fun queryInstances(
|
||||
beginMillis: Long,
|
||||
endMillis: Long,
|
||||
extraSelection: String?,
|
||||
args: Array<String>?,
|
||||
): List<EventInstance> {
|
||||
ensureObserversRegistered()
|
||||
// Hide cancelled occurrences: "delete only this event" writes a
|
||||
// cancelled exception for the one instance (#47). A NULL status is a
|
||||
// normal, un-cancelled event, so it must survive the filter — a bare
|
||||
// `!= CANCELED` would drop it (NULL != 2 is NULL, not true).
|
||||
// DELETED = 0: a provider that soft-deletes may keep the row's
|
||||
// instances until a purge (#364).
|
||||
val visible = "(${CalendarContract.Instances.STATUS} IS NULL OR " +
|
||||
"${CalendarContract.Instances.STATUS} != ${CalendarContract.Events.STATUS_CANCELED}) AND " +
|
||||
"${CalendarContract.Events.DELETED} = 0"
|
||||
return resolver.query(
|
||||
instancesUri(beginMillis, endMillis),
|
||||
InstanceProjection.COLUMNS,
|
||||
// Hide cancelled occurrences: "delete only this event" writes a
|
||||
// cancelled exception for the one instance (#47). A NULL status is a
|
||||
// normal, un-cancelled event, so it must survive the filter — a bare
|
||||
// `!= CANCELED` would drop it (NULL != 2 is NULL, not true).
|
||||
// DELETED = 0: a provider that soft-deletes may keep the row's
|
||||
// instances until a purge (#364).
|
||||
"(${CalendarContract.Instances.STATUS} IS NULL OR " +
|
||||
"${CalendarContract.Instances.STATUS} != ${CalendarContract.Events.STATUS_CANCELED}) AND " +
|
||||
"${CalendarContract.Events.DELETED} = 0",
|
||||
null,
|
||||
if (extraSelection == null) visible else "$visible AND $extraSelection",
|
||||
args,
|
||||
CalendarContract.Instances.BEGIN + " ASC",
|
||||
)?.use { c -> c.mapRowsNotNull { toEventInstance() } } ?: emptyList()
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ interface CalendarRepository {
|
||||
fun calendars(): Flow<List<CalendarSource>>
|
||||
fun instances(range: ClosedRange<Instant>): Flow<List<EventInstance>>
|
||||
|
||||
/** One read of event [eventId]'s occurrences overlapping [range], soonest first, whatever calendar it is in. */
|
||||
suspend fun eventOccurrences(eventId: Long, range: ClosedRange<Instant>): List<EventInstance>
|
||||
|
||||
/** Re-query every open [calendars] / [instances] flow. */
|
||||
fun refresh()
|
||||
|
||||
|
||||
@@ -194,6 +194,11 @@ class CalendarRepositoryImpl @Inject constructor(
|
||||
cachedCalendars
|
||||
}
|
||||
|
||||
override suspend fun eventOccurrences(eventId: Long, range: ClosedRange<Instant>): List<EventInstance> =
|
||||
withContext(io) {
|
||||
dataSource.eventInstances(eventId, range.start.toEpochMillis(), range.endInclusive.toEpochMillis())
|
||||
}
|
||||
|
||||
override suspend fun eventDetail(eventId: Long): EventDetail = withContext(io) {
|
||||
dataSource.eventDetail(eventId, allDayReminderTimeMinutes())
|
||||
?: throw NoSuchEventException(eventId)
|
||||
|
||||
@@ -101,9 +101,10 @@ fun TimeZonePickerDialog(
|
||||
onDismiss = onDismiss,
|
||||
scrollable = false,
|
||||
) {
|
||||
SearchField(
|
||||
PickerSearchField(
|
||||
query = query,
|
||||
onQueryChange = { query = it },
|
||||
placeholder = stringResource(R.string.event_edit_timezone_search),
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
)
|
||||
LazyColumn(contentPadding = PaddingValues(top = 8.dp, bottom = 24.dp)) {
|
||||
@@ -181,9 +182,10 @@ fun TimeZonePickerDialog(
|
||||
* which has a top bar to put it in) because the picker's bar is the title.
|
||||
*/
|
||||
@Composable
|
||||
private fun SearchField(
|
||||
internal fun PickerSearchField(
|
||||
query: String,
|
||||
onQueryChange: (String) -> Unit,
|
||||
placeholder: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val keyboard = LocalSoftwareKeyboardController.current
|
||||
@@ -206,7 +208,7 @@ private fun SearchField(
|
||||
InlineTextField(
|
||||
value = query,
|
||||
onValueChange = onQueryChange,
|
||||
placeholder = stringResource(R.string.event_edit_timezone_search),
|
||||
placeholder = placeholder,
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Search,
|
||||
onImeAction = { keyboard?.hide() },
|
||||
|
||||
@@ -29,11 +29,10 @@ import de.jeanlucmakiola.floret.components.GroupedSectionHeader
|
||||
import de.jeanlucmakiola.floret.components.positionOf
|
||||
import de.jeanlucmakiola.calendula.widget.config.PlacedWidget
|
||||
import de.jeanlucmakiola.calendula.widget.config.WidgetConfigActivity
|
||||
import de.jeanlucmakiola.calendula.widget.config.WidgetKind
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetKind
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import de.jeanlucmakiola.calendula.widget.config.backgroundLabel
|
||||
import de.jeanlucmakiola.calendula.widget.config.label
|
||||
import de.jeanlucmakiola.calendula.widget.config.placedWidgets
|
||||
import de.jeanlucmakiola.calendula.widget.config.themeLabel
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package de.jeanlucmakiola.calendula.widget
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Context
|
||||
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||
import de.jeanlucmakiola.floret.glance.reloadOnUpdate
|
||||
import de.jeanlucmakiola.floret.glance.restoreState
|
||||
|
||||
/**
|
||||
* What every widget receiver shares: the midnight rollover follows whether any
|
||||
* widget is placed (#228), a restore keeps each widget's settings (#292), and
|
||||
* the system's `updatePeriodMillis` wake-up reloads the data and re-arms the
|
||||
* alarm — anything that drops a pending alarm without a broadcast (force-stop,
|
||||
* battery restriction, an OEM freeze) is repaired there rather than on the
|
||||
* next app open.
|
||||
*/
|
||||
abstract class CalendulaWidgetReceiver : GlanceAppWidgetReceiver() {
|
||||
|
||||
override fun onEnabled(context: Context) {
|
||||
super.onEnabled(context)
|
||||
syncWidgetRollover(context)
|
||||
}
|
||||
|
||||
override fun onDisabled(context: Context) {
|
||||
super.onDisabled(context)
|
||||
syncWidgetRollover(context)
|
||||
}
|
||||
|
||||
override fun onRestored(context: Context, oldWidgetIds: IntArray, newWidgetIds: IntArray) {
|
||||
restoreState(context, oldWidgetIds, newWidgetIds)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingSuperCall")
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
) {
|
||||
// In place of super, which only recomposes and would take the goAsync handle.
|
||||
reloadOnUpdate(context, appWidgetIds)
|
||||
syncWidgetRollover(context)
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,12 @@ import androidx.datastore.preferences.core.MutablePreferences
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||
import androidx.glance.GlanceId
|
||||
import androidx.glance.appwidget.state.getAppWidgetState
|
||||
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||
import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay
|
||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||
import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange
|
||||
import de.jeanlucmakiola.calendula.ui.agenda.parseAgendaRange
|
||||
import de.jeanlucmakiola.calendula.ui.agenda.storageValue
|
||||
@@ -50,6 +52,7 @@ private val AgendaRangeKey = stringPreferencesKey("cfg_agenda_range")
|
||||
private val TextSizeKey = stringPreferencesKey("cfg_text_size")
|
||||
private val ShowTodayKey = booleanPreferencesKey("cfg_show_today")
|
||||
private val PastDisplayKey = stringPreferencesKey("cfg_past_display")
|
||||
private val HiddenCalendarsKey = stringSetPreferencesKey("cfg_hidden_calendars")
|
||||
|
||||
internal val Preferences.widgetAppearance: WidgetAppearance
|
||||
get() = WidgetAppearance(
|
||||
@@ -84,5 +87,19 @@ internal fun MutablePreferences.putAgendaOptions(options: AgendaWidgetOptions) {
|
||||
this[PastDisplayKey] = options.pastDisplay.name
|
||||
}
|
||||
|
||||
/**
|
||||
* Calendars this widget leaves out, on top of the ones hidden app-wide. Stored
|
||||
* as the excluded set, so a calendar added later shows up by default.
|
||||
*/
|
||||
internal val Preferences.hiddenCalendars: Set<Long>
|
||||
get() = this[HiddenCalendarsKey]?.mapNotNull { it.toLongOrNull() }?.toSet().orEmpty()
|
||||
|
||||
internal fun MutablePreferences.putHiddenCalendars(ids: Set<Long>) {
|
||||
this[HiddenCalendarsKey] = ids.map(Long::toString).toSet()
|
||||
}
|
||||
|
||||
internal fun List<EventInstance>.withoutCalendars(hidden: Set<Long>): List<EventInstance> =
|
||||
if (hidden.isEmpty()) this else filterNot { it.calendarId in hidden }
|
||||
|
||||
internal suspend fun Context.widgetState(id: GlanceId): Preferences =
|
||||
getAppWidgetState(this, PreferencesGlanceStateDefinition, id)
|
||||
|
||||
@@ -96,6 +96,12 @@ internal val SettingsPrefs.widgetSettings: Flow<WidgetSettings>
|
||||
|
||||
internal fun systemZone(): TimeZone = TimeZone.currentSystemDefault()
|
||||
|
||||
/**
|
||||
* The locale widget text is written in. Glance has no LocalConfiguration, and a
|
||||
* widget is redrawn on a locale change anyway (LOCALE_CHANGED), so a plain read is enough.
|
||||
*/
|
||||
internal fun widgetLocale(): Locale = Locale.getDefault()
|
||||
|
||||
internal fun today(zone: TimeZone): LocalDate =
|
||||
Clock.System.now().toLocalDateTime(zone).date
|
||||
|
||||
@@ -145,8 +151,9 @@ sealed interface MonthWidgetSource {
|
||||
/** [loadAgendaWidgetData] with agenda widget [id]'s own options. */
|
||||
internal suspend fun Context.loadAgendaWidget(id: GlanceId): Themed<AgendaWidgetData> {
|
||||
val settings = widgetEntryPoint().settingsPrefs().widgetSettings.first()
|
||||
val options = widgetState(id).agendaWidgetOptions(settings.agendaDefaults)
|
||||
return Themed(loadAgendaWidgetData(settings, options), settings.themeMode)
|
||||
val state = widgetState(id)
|
||||
val options = state.agendaWidgetOptions(settings.agendaDefaults)
|
||||
return Themed(loadAgendaWidgetData(settings, options, state.hiddenCalendars), settings.themeMode)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,6 +164,7 @@ internal suspend fun Context.loadAgendaWidget(id: GlanceId): Themed<AgendaWidget
|
||||
internal suspend fun Context.loadAgendaWidgetData(
|
||||
settings: WidgetSettings,
|
||||
options: AgendaWidgetOptions,
|
||||
hiddenCalendars: Set<Long>,
|
||||
): AgendaWidgetData {
|
||||
if (!hasCalendarPermission()) return AgendaWidgetData.NeedsPermission
|
||||
val zone = systemZone()
|
||||
@@ -166,7 +174,7 @@ internal suspend fun Context.loadAgendaWidgetData(
|
||||
val dayCount = options.range.dayCount(anchor, weekStart)
|
||||
val window = agendaRange(anchor, dayCount - 1, zone)
|
||||
val windowEnd = anchor.plus(dayCount - 1, DateTimeUnit.DAY)
|
||||
val instances = ep.calendarRepository().instances(window).first()
|
||||
val instances = ep.calendarRepository().instances(window).first().withoutCalendars(hiddenCalendars)
|
||||
return AgendaWidgetData.Ready(
|
||||
today = anchor,
|
||||
days = groupAgendaDays(anchor, windowEnd, instances, zone),
|
||||
@@ -214,15 +222,20 @@ private object MonthSourceCache {
|
||||
/** [loadThemedMonthSource] for widget [id], served from [MonthSourceCache] while its [stamp] holds. */
|
||||
internal suspend fun Context.loadCachedMonthSource(id: GlanceId, centerIndex: Int, stamp: Long): Themed<MonthWidgetSource> =
|
||||
MonthSourceCache.get(id, centerIndex, stamp)
|
||||
?: loadThemedMonthSource(centerIndex).also { MonthSourceCache.put(id, centerIndex, stamp, it) }
|
||||
?: loadThemedMonthSource(centerIndex, widgetState(id).hiddenCalendars)
|
||||
.also { MonthSourceCache.put(id, centerIndex, stamp, it) }
|
||||
|
||||
/** Read backing the month widget's grid for the window around [centerIndex], with the app theme. */
|
||||
internal suspend fun Context.loadThemedMonthSource(centerIndex: Int): Themed<MonthWidgetSource> {
|
||||
internal suspend fun Context.loadThemedMonthSource(centerIndex: Int, hiddenCalendars: Set<Long>): Themed<MonthWidgetSource> {
|
||||
val settings = widgetEntryPoint().settingsPrefs().widgetSettings.first()
|
||||
return Themed(queryMonthSource(centerIndex, settings), settings.themeMode)
|
||||
return Themed(queryMonthSource(centerIndex, settings, hiddenCalendars), settings.themeMode)
|
||||
}
|
||||
|
||||
private suspend fun Context.queryMonthSource(centerIndex: Int, settings: WidgetSettings): MonthWidgetSource {
|
||||
private suspend fun Context.queryMonthSource(
|
||||
centerIndex: Int,
|
||||
settings: WidgetSettings,
|
||||
hiddenCalendars: Set<Long>,
|
||||
): MonthWidgetSource {
|
||||
if (!hasCalendarPermission()) return MonthWidgetSource.NeedsPermission
|
||||
val zone = systemZone()
|
||||
val ep = widgetEntryPoint()
|
||||
@@ -239,7 +252,75 @@ private suspend fun Context.queryMonthSource(centerIndex: Int, settings: WidgetS
|
||||
today = today(zone),
|
||||
weekStart = settings.weekStart.resolveFirstDay(Locale.getDefault()),
|
||||
soften = settings.soften,
|
||||
instances = ep.calendarRepository().instances(from..to).first(),
|
||||
instances = ep.calendarRepository().instances(from..to).first().withoutCalendars(hiddenCalendars),
|
||||
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)
|
||||
|
||||
/** What a widget drawing a run of days loads: those days' events, grouped like the agenda. */
|
||||
sealed interface DaysWidgetData {
|
||||
data object NeedsPermission : DaysWidgetData
|
||||
data class Ready(
|
||||
val today: LocalDate,
|
||||
val firstDay: LocalDate,
|
||||
val lastDay: LocalDate,
|
||||
/** Only the days with events, in order. */
|
||||
val days: List<AgendaDay>,
|
||||
val weekStart: DayOfWeek,
|
||||
val is24Hour: Boolean,
|
||||
val soften: Boolean,
|
||||
val options: AgendaWidgetOptions,
|
||||
val now: Instant,
|
||||
) : DaysWidgetData {
|
||||
fun eventsOn(date: LocalDate): List<EventInstance> = days.firstOrNull { it.date == date }?.events.orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
/** Widget [id]'s stored options and calendars, with the app's values for what it has none of. */
|
||||
internal class WidgetLoadContext(val settings: WidgetSettings, val options: AgendaWidgetOptions, val hiddenCalendars: Set<Long>)
|
||||
|
||||
internal suspend fun Context.widgetLoadContext(id: GlanceId): WidgetLoadContext {
|
||||
val settings = widgetEntryPoint().settingsPrefs().widgetSettings.first()
|
||||
val state = widgetState(id)
|
||||
return WidgetLoadContext(settings, state.agendaWidgetOptions(settings.agendaDefaults), state.hiddenCalendars)
|
||||
}
|
||||
|
||||
/** The events on the days [days] picks from today and the week start, for [load]'s widget. */
|
||||
internal suspend fun Context.loadDays(
|
||||
load: WidgetLoadContext,
|
||||
days: (today: LocalDate, weekStart: DayOfWeek) -> ClosedRange<LocalDate>,
|
||||
): Themed<DaysWidgetData> {
|
||||
val settings = load.settings
|
||||
if (!hasCalendarPermission()) return Themed(DaysWidgetData.NeedsPermission, settings.themeMode)
|
||||
val zone = systemZone()
|
||||
val anchor = today(zone)
|
||||
val weekStart = settings.weekStart.resolveFirstDay(Locale.getDefault())
|
||||
val range = days(anchor, weekStart)
|
||||
val window = range.start.atStartOfDayIn(zone)..range.endInclusive.atTime(23, 59, 59).toInstant(zone)
|
||||
val instances = widgetEntryPoint().calendarRepository().instances(window).first().withoutCalendars(load.hiddenCalendars)
|
||||
return Themed(
|
||||
DaysWidgetData.Ready(
|
||||
today = anchor,
|
||||
firstDay = range.start,
|
||||
lastDay = range.endInclusive,
|
||||
days = groupAgendaDays(range.start, range.endInclusive, instances, zone),
|
||||
weekStart = weekStart,
|
||||
is24Hour = settings.timeFormat.is24Hour(android.text.format.DateFormat.is24HourFormat(this)),
|
||||
soften = settings.soften,
|
||||
options = load.options,
|
||||
now = Clock.System.now(),
|
||||
),
|
||||
settings.themeMode,
|
||||
)
|
||||
}
|
||||
|
||||
/** The week [today] falls in, from [weekStart]. */
|
||||
internal fun weekOf(today: LocalDate, weekStart: DayOfWeek): ClosedRange<LocalDate> {
|
||||
val back = (today.dayOfWeek.ordinal - weekStart.ordinal + 7) % 7
|
||||
val first = today.minus(back, DateTimeUnit.DAY)
|
||||
return first..first.plus(6, DateTimeUnit.DAY)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package de.jeanlucmakiola.calendula.widget
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.GlanceTheme
|
||||
import androidx.glance.LocalContext
|
||||
import androidx.glance.action.clickable
|
||||
import androidx.glance.appwidget.action.actionStartActivity
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.Column
|
||||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.height
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.text.FontWeight
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextStyle
|
||||
import de.jeanlucmakiola.calendula.MainActivity
|
||||
import de.jeanlucmakiola.calendula.R
|
||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||
import de.jeanlucmakiola.calendula.domain.isDeclined
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
||||
import de.jeanlucmakiola.calendula.ui.common.SECONDARY_INK_ALPHA
|
||||
import de.jeanlucmakiola.calendula.ui.common.eventFill
|
||||
import de.jeanlucmakiola.calendula.ui.common.eventInk
|
||||
import de.jeanlucmakiola.calendula.widget.agenda.metricsFor
|
||||
import de.jeanlucmakiola.floret.components.Position
|
||||
import de.jeanlucmakiola.floret.glance.WidgetGroupedRow
|
||||
import de.jeanlucmakiola.floret.glance.themedColor
|
||||
|
||||
/** Text and row sizes of a compact event list, from a widget's [WidgetSize]. */
|
||||
internal data class EventRowMetrics(val title: TextUnit, val time: TextUnit, val rowHeight: Dp, val message: TextUnit)
|
||||
|
||||
/** The agenda widget's sizes with tighter rows, resolved against the system font scale. */
|
||||
internal fun compactRowMetrics(size: WidgetSize, fontScale: Float): EventRowMetrics {
|
||||
val agenda = metricsFor(size)
|
||||
return EventRowMetrics(
|
||||
title = agenda.eventTitle,
|
||||
time = agenda.eventTime,
|
||||
rowHeight = agenda.contentH * fontScale + agenda.rowVPad,
|
||||
message = agenda.message,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* One event as a two-line row in its calendar's fill, part of a grouped card
|
||||
* at [position]; opens the occurrence. [quiet] draws it in the card colour
|
||||
* instead, for events shown only as a look ahead.
|
||||
*/
|
||||
@Composable
|
||||
internal fun WidgetEventRow(
|
||||
event: EventInstance,
|
||||
secondary: String,
|
||||
soften: Boolean,
|
||||
metrics: EventRowMetrics,
|
||||
position: Position,
|
||||
source: CalendarView,
|
||||
dimmed: Boolean = false,
|
||||
quiet: Boolean = false,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
fun tone(dark: Boolean): Color = eventFill(event.color, dark, soften).let { if (dimmed) it.copy(alpha = EventDimAlpha) else it }
|
||||
val day = tone(dark = false)
|
||||
val night = tone(dark = true)
|
||||
// Ink is picked against the opaque fill, then carries the dim along with it.
|
||||
fun ink(fill: Color, alpha: Float) = eventInk(fill.copy(alpha = 1f), alpha * fill.alpha)
|
||||
val card = widgetCardColor()
|
||||
WidgetGroupedRow(position = position, color = if (quiet) card else themedColor(day = day, night = night)) {
|
||||
Column(
|
||||
modifier = GlanceModifier
|
||||
.fillMaxWidth()
|
||||
.height(metrics.rowHeight)
|
||||
.padding(horizontal = 12.dp)
|
||||
.clickable(
|
||||
actionStartActivity(
|
||||
MainActivity.openEventIntent(
|
||||
context = context,
|
||||
eventId = event.eventId,
|
||||
beginMillis = event.start.toEpochMilliseconds(),
|
||||
endMillis = event.end.toEpochMilliseconds(),
|
||||
source = source,
|
||||
),
|
||||
),
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val titleInk = if (quiet) {
|
||||
GlanceTheme.colors.onSurface
|
||||
} else {
|
||||
themedColor(day = ink(day, 1f), night = ink(night, 1f))
|
||||
}
|
||||
val timeInk = if (quiet) {
|
||||
GlanceTheme.colors.onSurfaceVariant
|
||||
} else {
|
||||
themedColor(day = ink(day, SECONDARY_INK_ALPHA), night = ink(night, SECONDARY_INK_ALPHA))
|
||||
}
|
||||
Text(
|
||||
text = event.title.ifBlank { context.getString(R.string.event_untitled) },
|
||||
maxLines = 1,
|
||||
style = TextStyle(
|
||||
color = titleInk,
|
||||
fontSize = metrics.title,
|
||||
fontWeight = FontWeight.Medium,
|
||||
textDecoration = glanceDeclinedDecoration(event.isDeclined),
|
||||
),
|
||||
)
|
||||
Text(text = secondary, maxLines = 1, style = TextStyle(color = timeInk, fontSize = metrics.time))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package de.jeanlucmakiola.calendula.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Context
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import de.jeanlucmakiola.calendula.R
|
||||
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
|
||||
import de.jeanlucmakiola.calendula.widget.date.DateWidget
|
||||
import de.jeanlucmakiola.calendula.widget.date.DateWidgetReceiver
|
||||
import de.jeanlucmakiola.calendula.widget.week.WeekStripWidget
|
||||
import de.jeanlucmakiola.calendula.widget.week.WeekStripWidgetReceiver
|
||||
import de.jeanlucmakiola.calendula.widget.upcoming.UpcomingWidget
|
||||
import de.jeanlucmakiola.calendula.widget.upcoming.UpcomingWidgetReceiver
|
||||
import de.jeanlucmakiola.calendula.widget.today.TodayWidget
|
||||
import de.jeanlucmakiola.calendula.widget.today.TodayWidgetReceiver
|
||||
import de.jeanlucmakiola.calendula.widget.countdown.CountdownWidget
|
||||
import de.jeanlucmakiola.calendula.widget.countdown.CountdownWidgetReceiver
|
||||
import de.jeanlucmakiola.calendula.widget.split.MonthDayWidget
|
||||
import de.jeanlucmakiola.calendula.widget.split.MonthDayWidgetReceiver
|
||||
|
||||
/** A content option a widget's config screen offers, besides background and theme. */
|
||||
enum class WidgetOption { RANGE, TEXT_SIZE, PAST_DISPLAY, SHOW_TODAY, CALENDARS }
|
||||
|
||||
/**
|
||||
* Every widget the app ships, told apart by their receiver: what redraws,
|
||||
* the rollover and the config screen go through.
|
||||
*/
|
||||
enum class WidgetKind(
|
||||
val receiver: Class<*>,
|
||||
val label: Int,
|
||||
val options: Set<WidgetOption>,
|
||||
/** The size a config preview draws at before the launcher reports one. */
|
||||
val defaultSize: DpSize,
|
||||
) {
|
||||
AGENDA(
|
||||
AgendaWidgetReceiver::class.java,
|
||||
R.string.widget_agenda_label,
|
||||
setOf(WidgetOption.RANGE, WidgetOption.TEXT_SIZE, WidgetOption.PAST_DISPLAY, WidgetOption.SHOW_TODAY, WidgetOption.CALENDARS),
|
||||
DpSize(260.dp, 280.dp),
|
||||
),
|
||||
MONTH(
|
||||
MonthWidgetReceiver::class.java,
|
||||
R.string.widget_month_label,
|
||||
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),
|
||||
),
|
||||
DATE(
|
||||
DateWidgetReceiver::class.java,
|
||||
R.string.widget_date_label,
|
||||
emptySet(),
|
||||
DpSize(76.dp, 76.dp),
|
||||
),
|
||||
WEEK_STRIP(
|
||||
WeekStripWidgetReceiver::class.java,
|
||||
R.string.widget_week_strip_label,
|
||||
setOf(WidgetOption.CALENDARS),
|
||||
DpSize(300.dp, 90.dp),
|
||||
),
|
||||
UPCOMING(
|
||||
UpcomingWidgetReceiver::class.java,
|
||||
R.string.widget_upcoming_label,
|
||||
setOf(WidgetOption.RANGE, WidgetOption.TEXT_SIZE, WidgetOption.CALENDARS),
|
||||
DpSize(180.dp, 180.dp),
|
||||
),
|
||||
TODAY(
|
||||
TodayWidgetReceiver::class.java,
|
||||
R.string.widget_today_label,
|
||||
setOf(WidgetOption.TEXT_SIZE, WidgetOption.PAST_DISPLAY, WidgetOption.CALENDARS),
|
||||
DpSize(300.dp, 100.dp),
|
||||
),
|
||||
COUNTDOWN(
|
||||
CountdownWidgetReceiver::class.java,
|
||||
R.string.widget_countdown_label,
|
||||
setOf(WidgetOption.CALENDARS),
|
||||
DpSize(200.dp, 100.dp),
|
||||
),
|
||||
MONTH_DAY(
|
||||
MonthDayWidgetReceiver::class.java,
|
||||
R.string.widget_month_day_label,
|
||||
setOf(WidgetOption.TEXT_SIZE, WidgetOption.PAST_DISPLAY, WidgetOption.CALENDARS),
|
||||
DpSize(320.dp, 200.dp),
|
||||
),
|
||||
;
|
||||
|
||||
fun widget(): GlanceAppWidget = when (this) {
|
||||
AGENDA -> AgendaWidget()
|
||||
MONTH -> MonthWidget()
|
||||
QUICK_ADD -> QuickAddWidget()
|
||||
DATE -> DateWidget()
|
||||
WEEK_STRIP -> WeekStripWidget()
|
||||
UPCOMING -> UpcomingWidget()
|
||||
TODAY -> TodayWidget()
|
||||
COUNTDOWN -> CountdownWidget()
|
||||
MONTH_DAY -> MonthDayWidget()
|
||||
}
|
||||
|
||||
/** Whether this kind stores [AgendaWidgetOptions] of its own. */
|
||||
val hasContentOptions: Boolean
|
||||
get() = options.any { it != WidgetOption.CALENDARS }
|
||||
|
||||
companion object {
|
||||
fun of(context: Context, appWidgetId: Int): WidgetKind? {
|
||||
val provider = AppWidgetManager.getInstance(context)?.getAppWidgetInfo(appWidgetId)?.provider ?: return null
|
||||
return entries.firstOrNull { it.receiver.name == provider.className }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package de.jeanlucmakiola.calendula.widget
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.glance.GlanceTheme
|
||||
import androidx.glance.unit.ColorProvider
|
||||
import de.jeanlucmakiola.calendula.R
|
||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||
import de.jeanlucmakiola.calendula.ui.agenda.AgendaTimeLabel
|
||||
import de.jeanlucmakiola.calendula.ui.agenda.agendaTimeLabel
|
||||
import de.jeanlucmakiola.calendula.ui.common.formatTimeOfDay
|
||||
import de.jeanlucmakiola.floret.glance.LocalWidgetBackground
|
||||
import de.jeanlucmakiola.floret.glance.WidgetBackground
|
||||
import de.jeanlucmakiola.floret.locale.localizedDateFormatter
|
||||
import kotlinx.datetime.DateTimeUnit
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.plus
|
||||
import kotlinx.datetime.toJavaDayOfWeek
|
||||
import kotlinx.datetime.toJavaLocalDate
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import java.util.Locale
|
||||
import kotlin.time.Instant
|
||||
|
||||
/** Cards sit one tone off the root fill, and stay opaque over a wallpaper (#292). */
|
||||
@Composable
|
||||
internal fun widgetCardColor(): ColorProvider = when (LocalWidgetBackground.current) {
|
||||
WidgetBackground.SOLID -> GlanceTheme.colors.surfaceVariant
|
||||
WidgetBackground.TINTED, WidgetBackground.TRANSPARENT -> GlanceTheme.colors.widgetBackground
|
||||
}
|
||||
|
||||
/** An event's time on [day]: "All day", "Starts 14:00", "Ends 9:00" or "14:00 – 15:30". */
|
||||
internal fun widgetTimeLabel(context: Context, event: EventInstance, day: LocalDate, is24Hour: Boolean): String =
|
||||
when (val label = agendaTimeLabel(event, day, systemZone())) {
|
||||
AgendaTimeLabel.AllDay -> context.getString(R.string.event_detail_all_day)
|
||||
is AgendaTimeLabel.Starts -> context.getString(R.string.agenda_span_starts, formatWidgetTime(label.start, is24Hour))
|
||||
is AgendaTimeLabel.Ends -> context.getString(R.string.agenda_span_ends, formatWidgetTime(label.end, is24Hour))
|
||||
is AgendaTimeLabel.Range -> "${formatWidgetTime(label.start, is24Hour)} – ${formatWidgetTime(label.end, is24Hour)}"
|
||||
}
|
||||
|
||||
internal fun formatWidgetTime(instant: Instant, is24Hour: Boolean): String {
|
||||
val t = instant.toLocalDateTime(systemZone()).time
|
||||
return formatTimeOfDay(t.hour, t.minute, is24Hour, Locale.getDefault())
|
||||
}
|
||||
|
||||
/** "Today", "Tomorrow", or the short weekday within the coming week, else the short date. */
|
||||
internal fun relativeDayLabel(context: Context, date: LocalDate, today: LocalDate): String {
|
||||
val locale = Locale.getDefault()
|
||||
return when {
|
||||
date == today -> context.getString(R.string.agenda_header_today)
|
||||
date == today.plus(1, DateTimeUnit.DAY) -> context.getString(R.string.agenda_header_tomorrow)
|
||||
date < today.plus(7, DateTimeUnit.DAY) && date > today ->
|
||||
date.dayOfWeek.toJavaDayOfWeek().getDisplayName(java.time.format.TextStyle.SHORT, locale)
|
||||
else -> localizedDateFormatter(locale, "dMMM").format(date.toJavaLocalDate())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package de.jeanlucmakiola.calendula.widget
|
||||
|
||||
import android.content.Context
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.SizeMode
|
||||
import androidx.glance.appwidget.provideContent
|
||||
import de.jeanlucmakiola.calendula.R
|
||||
import de.jeanlucmakiola.calendula.data.prefs.is24Hour
|
||||
import de.jeanlucmakiola.calendula.data.prefs.resolveFirstDay
|
||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||
import de.jeanlucmakiola.calendula.ui.agenda.groupAgendaDays
|
||||
import de.jeanlucmakiola.calendula.widget.agenda.AgendaWidgetContent
|
||||
import de.jeanlucmakiola.calendula.widget.agenda.AgendaWidgetReceiver
|
||||
import de.jeanlucmakiola.calendula.widget.countdown.CountdownData
|
||||
import de.jeanlucmakiola.calendula.widget.countdown.CountdownOptions
|
||||
import de.jeanlucmakiola.calendula.widget.countdown.CountdownWidgetContent
|
||||
import de.jeanlucmakiola.calendula.widget.countdown.CountdownWidgetReceiver
|
||||
import de.jeanlucmakiola.calendula.widget.date.DateWidgetContent
|
||||
import de.jeanlucmakiola.calendula.widget.date.DateWidgetReceiver
|
||||
import de.jeanlucmakiola.calendula.widget.month.MonthWidgetContent
|
||||
import de.jeanlucmakiola.calendula.widget.month.MonthWidgetReceiver
|
||||
import de.jeanlucmakiola.calendula.widget.quickadd.QuickAddWidgetContent
|
||||
import de.jeanlucmakiola.calendula.widget.quickadd.QuickAddWidgetReceiver
|
||||
import de.jeanlucmakiola.calendula.widget.split.MonthDayData
|
||||
import de.jeanlucmakiola.calendula.widget.split.MonthDayWidgetContent
|
||||
import de.jeanlucmakiola.calendula.widget.split.MonthDayWidgetReceiver
|
||||
import de.jeanlucmakiola.calendula.widget.today.TodayWidgetContent
|
||||
import de.jeanlucmakiola.calendula.widget.today.TodayWidgetReceiver
|
||||
import de.jeanlucmakiola.calendula.widget.upcoming.UpcomingWidgetContent
|
||||
import de.jeanlucmakiola.calendula.widget.upcoming.UpcomingWidgetReceiver
|
||||
import de.jeanlucmakiola.calendula.widget.week.WeekStripWidgetContent
|
||||
import de.jeanlucmakiola.calendula.widget.week.WeekStripWidgetReceiver
|
||||
import de.jeanlucmakiola.floret.glance.publishWidgetPreviews
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.datetime.DateTimeUnit
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.LocalTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.atStartOfDayIn
|
||||
import kotlinx.datetime.atTime
|
||||
import kotlinx.datetime.plus
|
||||
import kotlinx.datetime.toInstant
|
||||
import java.util.Locale
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
/** Republishes the generated picker previews (Android 15+) after an app update or a language change. */
|
||||
internal suspend fun publishCalendulaWidgetPreviews(context: Context) {
|
||||
publishWidgetPreviews(
|
||||
context,
|
||||
version = "${context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime}-${Locale.getDefault().toLanguageTag()}",
|
||||
receivers = listOf(
|
||||
AgendaWidgetReceiver::class,
|
||||
MonthWidgetReceiver::class,
|
||||
QuickAddWidgetReceiver::class,
|
||||
DateWidgetReceiver::class,
|
||||
WeekStripWidgetReceiver::class,
|
||||
UpcomingWidgetReceiver::class,
|
||||
TodayWidgetReceiver::class,
|
||||
CountdownWidgetReceiver::class,
|
||||
MonthDayWidgetReceiver::class,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/** The size [kind]'s picker preview is laid out at. */
|
||||
internal fun previewSizeOf(kind: WidgetKind) = SizeMode.Responsive(setOf(kind.defaultSize))
|
||||
|
||||
/** [kind]'s picker preview: the real widget, drawn with sample events around today's morning. */
|
||||
internal suspend fun GlanceAppWidget.provideSamplePreview(context: Context, kind: WidgetKind) {
|
||||
val settings = context.widgetEntryPoint().settingsPrefs().widgetSettings.first()
|
||||
val zone = systemZone()
|
||||
val today = today(zone)
|
||||
val now = today.atTime(8, 0).toInstant(zone)
|
||||
val instances = context.sampleInstances(today, zone)
|
||||
val weekStart = settings.weekStart.resolveFirstDay(Locale.getDefault())
|
||||
val is24Hour = settings.timeFormat.is24Hour(android.text.format.DateFormat.is24HourFormat(context))
|
||||
val options = settings.agendaDefaults
|
||||
val appearance = WidgetAppearance()
|
||||
val index = monthIndexOf(today)
|
||||
val month = MonthWidgetSource.Ready(today, weekStart, settings.soften, instances, index..index)
|
||||
fun days(range: ClosedRange<LocalDate>) = DaysWidgetData.Ready(
|
||||
today = today,
|
||||
firstDay = range.start,
|
||||
lastDay = range.endInclusive,
|
||||
days = groupAgendaDays(range.start, range.endInclusive, instances, zone),
|
||||
weekStart = weekStart,
|
||||
is24Hour = is24Hour,
|
||||
soften = settings.soften,
|
||||
options = options,
|
||||
now = now,
|
||||
)
|
||||
val theme = settings.themeMode
|
||||
provideContent {
|
||||
when (kind) {
|
||||
WidgetKind.AGENDA -> AgendaWidgetContent(
|
||||
AgendaWidgetData.Ready(
|
||||
today = today,
|
||||
days = groupAgendaDays(today, today.plus(6, DateTimeUnit.DAY), instances, zone),
|
||||
is24Hour = is24Hour,
|
||||
soften = settings.soften,
|
||||
pastDisplay = options.pastDisplay,
|
||||
showToday = options.showToday,
|
||||
textSize = options.textSize,
|
||||
now = now,
|
||||
),
|
||||
appearance,
|
||||
theme,
|
||||
preview = true,
|
||||
)
|
||||
WidgetKind.MONTH -> MonthWidgetContent(month, index, appearance, theme)
|
||||
WidgetKind.MONTH_DAY -> MonthDayWidgetContent(
|
||||
MonthDayData.Ready(month, options, is24Hour, now),
|
||||
index,
|
||||
selected = null,
|
||||
appearance = appearance,
|
||||
appTheme = theme,
|
||||
preview = true,
|
||||
)
|
||||
WidgetKind.QUICK_ADD -> QuickAddWidgetContent(today, appearance, theme)
|
||||
WidgetKind.DATE -> DateWidgetContent(today, appearance, theme)
|
||||
WidgetKind.WEEK_STRIP -> WeekStripWidgetContent(days(weekOf(today, weekStart)), appearance, theme)
|
||||
WidgetKind.UPCOMING -> UpcomingWidgetContent(days(today..today.plus(6, DateTimeUnit.DAY)), appearance, theme)
|
||||
WidgetKind.TODAY -> TodayWidgetContent(days(today..today.plus(1, DateTimeUnit.DAY)), appearance, theme, preview = true)
|
||||
WidgetKind.COUNTDOWN -> CountdownWidgetContent(
|
||||
CountdownData.Ready(
|
||||
options = CountdownOptions(),
|
||||
event = instances.first { !it.isAllDay && it.start > now },
|
||||
now = now,
|
||||
today = today,
|
||||
is24Hour = is24Hour,
|
||||
soften = settings.soften,
|
||||
appWidgetId = null,
|
||||
),
|
||||
appearance,
|
||||
theme,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A plausible few weeks: a busy today, a trip tomorrow, and a scatter of days with something on. */
|
||||
private fun Context.sampleInstances(today: LocalDate, zone: TimeZone): List<EventInstance> {
|
||||
var id = 0L
|
||||
fun timed(day: Int, at: LocalTime, length: kotlin.time.Duration, title: Int, color: Int): EventInstance {
|
||||
val start = today.plus(day, DateTimeUnit.DAY).atTime(at).toInstant(zone)
|
||||
id++
|
||||
return EventInstance(id, id, 1, getString(title), start, start + length, false, color, null)
|
||||
}
|
||||
fun allDay(day: Int, title: Int, color: Int): EventInstance {
|
||||
val date = today.plus(day, DateTimeUnit.DAY)
|
||||
id++
|
||||
return EventInstance(
|
||||
id, id, 1, getString(title),
|
||||
date.atStartOfDayIn(TimeZone.UTC), date.plus(1, DateTimeUnit.DAY).atStartOfDayIn(TimeZone.UTC),
|
||||
true, color, null,
|
||||
)
|
||||
}
|
||||
return listOf(
|
||||
timed(0, LocalTime(9, 30), 30.minutes, R.string.settings_past_events_sample_morning, Blue),
|
||||
timed(0, LocalTime(12, 30), 1.hours, R.string.settings_past_events_sample_midday, Green),
|
||||
timed(0, LocalTime(19, 0), 2.hours, R.string.settings_past_events_sample_evening, Purple),
|
||||
allDay(1, R.string.widget_preview_sample_trip, Orange),
|
||||
timed(2, LocalTime(10, 0), 1.hours, R.string.widget_preview_sample_dentist, Red),
|
||||
timed(-6, LocalTime(9, 30), 30.minutes, R.string.settings_past_events_sample_morning, Blue),
|
||||
timed(-2, LocalTime(19, 0), 2.hours, R.string.settings_past_events_sample_evening, Purple),
|
||||
timed(5, LocalTime(19, 0), 2.hours, R.string.settings_past_events_sample_evening, Purple),
|
||||
timed(8, LocalTime(9, 30), 30.minutes, R.string.settings_past_events_sample_morning, Blue),
|
||||
timed(12, LocalTime(12, 30), 1.hours, R.string.settings_past_events_sample_midday, Green),
|
||||
).sortedBy { it.start }
|
||||
}
|
||||
|
||||
private const val Blue = 0xFF039BE5.toInt()
|
||||
private const val Green = 0xFF33B679.toInt()
|
||||
private const val Purple = 0xFF8E24AA.toInt()
|
||||
private const val Orange = 0xFFF4511E.toInt()
|
||||
private const val Red = 0xFFD50000.toInt()
|
||||
@@ -9,10 +9,6 @@ import android.os.SystemClock
|
||||
import de.jeanlucmakiola.calendula.data.calendar.CalendarRepository
|
||||
import de.jeanlucmakiola.calendula.data.di.ApplicationScope
|
||||
import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
|
||||
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.floret.glance.WidgetRefresher
|
||||
import de.jeanlucmakiola.floret.glance.WidgetRollover
|
||||
import de.jeanlucmakiola.floret.glance.updatePlaced
|
||||
@@ -33,10 +29,7 @@ import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** Every widget receiver; the set [WidgetRollover] checks for placed instances. */
|
||||
internal val WIDGET_RECEIVER_CLASSES: List<Class<*>> = listOf(
|
||||
MonthWidgetReceiver::class.java,
|
||||
AgendaWidgetReceiver::class.java,
|
||||
)
|
||||
internal val WIDGET_RECEIVER_CLASSES: List<Class<*>> = WidgetKind.entries.map { it.receiver }
|
||||
|
||||
/** The app's own midnight wake-up (#228). */
|
||||
internal const val ACTION_WIDGET_ROLLOVER = "de.jeanlucmakiola.calendula.widget.ROLLOVER"
|
||||
@@ -57,31 +50,28 @@ internal fun syncWidgetRollover(context: Context) =
|
||||
|
||||
/**
|
||||
* Reloads and redraws every placed widget, a running session included: the
|
||||
* refresh stamp moves, and both widgets key their data load on it (#420).
|
||||
* refresh stamp moves, and every widget keys its data load on it (#420).
|
||||
*/
|
||||
internal suspend fun redrawWidgets(context: Context) {
|
||||
redrawAgendaWidgets(context)
|
||||
redrawMonthWidgets(context)
|
||||
WidgetKind.entries.forEach { redrawWidgets(context, it) }
|
||||
}
|
||||
|
||||
private suspend fun redrawAgendaWidgets(context: Context) =
|
||||
AgendaWidget().updatePlaced(context, AgendaWidgetReceiver::class.java, bumpRefreshStamp = true)
|
||||
internal suspend fun redrawWidgets(context: Context, kind: WidgetKind) =
|
||||
kind.widget().updatePlaced(context, kind.receiver, bumpRefreshStamp = true)
|
||||
|
||||
/** Redraws only the agenda widgets with no options of their own, which still follow the app's. */
|
||||
private suspend fun redrawUnconfiguredAgendaWidgets(context: Context) {
|
||||
/** Redraws only the widgets with no content options of their own, which still follow the app's. */
|
||||
private suspend fun redrawUnconfiguredWidgets(context: Context) {
|
||||
val appContext = context.applicationContext
|
||||
val ids = AppWidgetManager.getInstance(appContext)
|
||||
?.getAppWidgetIds(ComponentName(appContext, AgendaWidgetReceiver::class.java)) ?: return
|
||||
val manager = AppWidgetManager.getInstance(appContext) ?: return
|
||||
val glanceManager = GlanceAppWidgetManager(appContext)
|
||||
ids.forEach { appWidgetId ->
|
||||
val glanceId = glanceManager.getGlanceIdBy(appWidgetId)
|
||||
if (!appContext.widgetState(glanceId).hasAgendaOptions) AgendaWidget().updateWithRefresh(appContext, glanceId)
|
||||
WidgetKind.entries.filter { it.hasContentOptions }.forEach { kind ->
|
||||
manager.getAppWidgetIds(ComponentName(appContext, kind.receiver)).forEach { appWidgetId ->
|
||||
val glanceId = glanceManager.getGlanceIdBy(appWidgetId)
|
||||
if (!appContext.widgetState(glanceId).hasAgendaOptions) kind.widget().updateWithRefresh(appContext, glanceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun redrawMonthWidgets(context: Context) =
|
||||
MonthWidget().updatePlaced(context, MonthWidgetReceiver::class.java, bumpRefreshStamp = true)
|
||||
|
||||
/** When the app last saw a calendar change of its own, for skipping the `PROVIDER_CHANGED` that echoes it. */
|
||||
private val lastLocalChange = AtomicLong(0L)
|
||||
|
||||
@@ -97,7 +87,7 @@ internal fun isProviderEcho(): Boolean {
|
||||
* the settings they draw with, so those don't wait for `PROVIDER_CHANGED`,
|
||||
* which arrives seconds late or not at all (#420, #229). Changes from outside
|
||||
* the app are [WidgetUpdateReceiver]'s. The app's agenda options only redraw
|
||||
* agenda widgets that have none of their own (#292). Started once from
|
||||
* widgets that have no content options of their own (#292). Started once from
|
||||
* `CalendulaApp`.
|
||||
*/
|
||||
@Singleton
|
||||
@@ -112,20 +102,17 @@ class WidgetRefreshCoordinator @Inject constructor(
|
||||
/** Subscribes before returning, so a change made right after is never taken for the starting value. */
|
||||
fun start() {
|
||||
if (!started.compareAndSet(false, true)) return
|
||||
val agenda = WidgetRefresher(scope) { redrawAgendaWidgets(context) }
|
||||
val unconfiguredAgenda = WidgetRefresher(scope) { redrawUnconfiguredAgendaWidgets(context) }
|
||||
val month = WidgetRefresher(scope) { redrawMonthWidgets(context) }
|
||||
val all = WidgetKind.entries.map { kind -> WidgetRefresher(scope) { redrawWidgets(context, kind) } }
|
||||
val unconfigured = WidgetRefresher(scope) { redrawUnconfiguredWidgets(context) }
|
||||
watch(repository.localChanges) {
|
||||
lastLocalChange.set(SystemClock.elapsedRealtime())
|
||||
agenda.request()
|
||||
month.request()
|
||||
all.forEach { it.request() }
|
||||
}
|
||||
watch(prefs.widgetSettings.map { it.shared }.distinctUntilChanged().drop(1)) {
|
||||
agenda.request()
|
||||
month.request()
|
||||
all.forEach { it.request() }
|
||||
}
|
||||
watch(prefs.widgetSettings.map { it.agendaDefaults }.distinctUntilChanged().drop(1)) {
|
||||
unconfiguredAgenda.request()
|
||||
unconfigured.request()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package de.jeanlucmakiola.calendula.widget.agenda
|
||||
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetKind
|
||||
import de.jeanlucmakiola.calendula.widget.previewSizeOf
|
||||
import de.jeanlucmakiola.calendula.widget.provideSamplePreview
|
||||
import de.jeanlucmakiola.calendula.domain.isDeclined
|
||||
import de.jeanlucmakiola.calendula.widget.glanceDeclinedDecoration
|
||||
import android.content.Context
|
||||
@@ -45,13 +48,10 @@ import de.jeanlucmakiola.calendula.R
|
||||
import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay
|
||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||
import de.jeanlucmakiola.calendula.domain.hasEnded
|
||||
import de.jeanlucmakiola.calendula.ui.agenda.AgendaTimeLabel
|
||||
import de.jeanlucmakiola.calendula.ui.agenda.agendaTimeLabel
|
||||
import de.jeanlucmakiola.calendula.ui.agenda.anchorTodayIfMissing
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
||||
import de.jeanlucmakiola.calendula.ui.common.formatCalendarTitle
|
||||
import de.jeanlucmakiola.calendula.ui.common.formatTimeOfDay
|
||||
import de.jeanlucmakiola.floret.locale.localizedDateFormatter
|
||||
import de.jeanlucmakiola.calendula.ui.common.eventFill
|
||||
import de.jeanlucmakiola.calendula.ui.common.eventInk
|
||||
@@ -60,12 +60,12 @@ import de.jeanlucmakiola.calendula.widget.AgendaWidgetData
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetSize
|
||||
import de.jeanlucmakiola.calendula.widget.CalendulaWidgetRoot
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetAppearance
|
||||
import de.jeanlucmakiola.calendula.widget.widgetCardColor
|
||||
import de.jeanlucmakiola.calendula.widget.widgetTimeLabel
|
||||
import de.jeanlucmakiola.calendula.widget.loadAgendaWidget
|
||||
import de.jeanlucmakiola.calendula.widget.widgetAppearance
|
||||
import de.jeanlucmakiola.floret.components.Position
|
||||
import de.jeanlucmakiola.floret.components.positionOf
|
||||
import de.jeanlucmakiola.floret.glance.LocalWidgetBackground
|
||||
import de.jeanlucmakiola.floret.glance.WidgetBackground
|
||||
import de.jeanlucmakiola.floret.glance.WidgetGroupedRow
|
||||
import de.jeanlucmakiola.floret.glance.themedColor
|
||||
import de.jeanlucmakiola.floret.glance.wallpaperBacking
|
||||
@@ -79,11 +79,8 @@ import de.jeanlucmakiola.calendula.widget.systemZone
|
||||
import de.jeanlucmakiola.calendula.widget.today
|
||||
import kotlinx.datetime.DateTimeUnit
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.plus
|
||||
import kotlinx.datetime.toJavaDayOfWeek
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import kotlin.time.Instant
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
@@ -101,6 +98,11 @@ class AgendaWidget : GlanceAppWidget() {
|
||||
// the RemoteViews past the binder transaction limit.
|
||||
override val sizeMode = SizeMode.Single
|
||||
|
||||
override val previewSizeMode = previewSizeOf(WidgetKind.AGENDA)
|
||||
|
||||
override suspend fun providePreview(context: Context, widgetCategory: Int) =
|
||||
provideSamplePreview(context, WidgetKind.AGENDA)
|
||||
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
val stamp = refreshStamp(context, id)
|
||||
val widgetData = WidgetData(context.loadAgendaWidget(id), stamp)
|
||||
@@ -217,7 +219,7 @@ private fun AgendaRows(
|
||||
// Each day is one card of grouped rows (#71), with its date pill
|
||||
// beside the first row.
|
||||
val positions = dayPositions(rows)
|
||||
val cardColor = agendaCardColor()
|
||||
val cardColor = widgetCardColor()
|
||||
val item: @Composable (Int) -> Unit = { index ->
|
||||
val row = rows[index]
|
||||
val position = positions[index]
|
||||
@@ -305,13 +307,6 @@ private fun AgendaHeader() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Cards sit one tone off the root fill, and stay opaque over a wallpaper (#292). */
|
||||
@Composable
|
||||
private fun agendaCardColor(): ColorProvider = when (LocalWidgetBackground.current) {
|
||||
WidgetBackground.SOLID -> GlanceTheme.colors.surfaceVariant
|
||||
WidgetBackground.TINTED, WidgetBackground.TRANSPARENT -> GlanceTheme.colors.widgetBackground
|
||||
}
|
||||
|
||||
/** The month name ("November", with its year outside the current one) above a new month's first day. */
|
||||
@Composable
|
||||
private fun MonthLabel(date: LocalDate, today: LocalDate, metrics: AgendaMetrics) {
|
||||
@@ -470,8 +465,6 @@ private fun WidgetMessage(resId: Int, metrics: AgendaMetrics) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun zone(): TimeZone = systemZone()
|
||||
|
||||
/** "Today · Wed, 17 Jun" — relative word for today/tomorrow, else the date. */
|
||||
private fun agendaDayLabel(context: Context, date: LocalDate, today: LocalDate): String {
|
||||
val relative = when (date) {
|
||||
@@ -503,20 +496,7 @@ private fun eventTimeSummary(
|
||||
day: LocalDate,
|
||||
is24Hour: Boolean,
|
||||
): String {
|
||||
val time = when (val label = agendaTimeLabel(event, day, zone())) {
|
||||
AgendaTimeLabel.AllDay -> context.getString(R.string.event_detail_all_day)
|
||||
is AgendaTimeLabel.Starts ->
|
||||
context.getString(R.string.agenda_span_starts, formatTime(label.start, is24Hour))
|
||||
is AgendaTimeLabel.Ends ->
|
||||
context.getString(R.string.agenda_span_ends, formatTime(label.end, is24Hour))
|
||||
is AgendaTimeLabel.Range ->
|
||||
"${formatTime(label.start, is24Hour)} – ${formatTime(label.end, is24Hour)}"
|
||||
}
|
||||
val time = widgetTimeLabel(context, event, day, is24Hour)
|
||||
val location = event.location?.takeIf { it.isNotBlank() }
|
||||
return if (location != null) "$time · $location" else time
|
||||
}
|
||||
|
||||
private fun formatTime(instant: Instant, is24Hour: Boolean): String {
|
||||
val t = instant.toLocalDateTime(zone()).time
|
||||
return formatTimeOfDay(t.hour, t.minute, is24Hour, Locale.getDefault())
|
||||
}
|
||||
|
||||
+3
-48
@@ -1,54 +1,9 @@
|
||||
package de.jeanlucmakiola.calendula.widget.agenda
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Context
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||
import de.jeanlucmakiola.calendula.widget.syncWidgetRollover
|
||||
import de.jeanlucmakiola.floret.glance.reloadOnUpdate
|
||||
import de.jeanlucmakiola.floret.glance.restoreState
|
||||
import de.jeanlucmakiola.calendula.widget.CalendulaWidgetReceiver
|
||||
|
||||
/**
|
||||
* Host-facing receiver for the agenda widget. Declared in the manifest with the
|
||||
* `appwidget_info_agenda` provider metadata; delegates all rendering to
|
||||
* [AgendaWidget].
|
||||
*/
|
||||
class AgendaWidgetReceiver : GlanceAppWidgetReceiver() {
|
||||
/** Host-facing receiver for the agenda widget; rendering is [AgendaWidget]'s. */
|
||||
class AgendaWidgetReceiver : CalendulaWidgetReceiver() {
|
||||
override val glanceAppWidget: GlanceAppWidget = AgendaWidget()
|
||||
|
||||
/** First agenda widget placed — start rolling "today" over at midnight (#228). */
|
||||
override fun onEnabled(context: Context) {
|
||||
super.onEnabled(context)
|
||||
syncWidgetRollover(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Last agenda widget removed. [syncWidgetRollover] cancels only if
|
||||
* no month widget is left either.
|
||||
*/
|
||||
override fun onDisabled(context: Context) {
|
||||
super.onDisabled(context)
|
||||
syncWidgetRollover(context)
|
||||
}
|
||||
|
||||
/** A backup restore or device transfer renumbered the widgets: keep each one's settings (#292). */
|
||||
override fun onRestored(context: Context, oldWidgetIds: IntArray, newWidgetIds: IntArray) {
|
||||
restoreState(context, oldWidgetIds, newWidgetIds)
|
||||
}
|
||||
|
||||
/**
|
||||
* Self-heal on the system's own `updatePeriodMillis` wake-up — see
|
||||
* [de.jeanlucmakiola.calendula.widget.month.MonthWidgetReceiver.onUpdate].
|
||||
*/
|
||||
@SuppressLint("MissingSuperCall")
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
) {
|
||||
// In place of super, which only recomposes and would take the goAsync handle.
|
||||
reloadOnUpdate(context, appWidgetIds)
|
||||
syncWidgetRollover(context)
|
||||
}
|
||||
}
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
package de.jeanlucmakiola.calendula.widget.config
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.jeanlucmakiola.calendula.R
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip
|
||||
import de.jeanlucmakiola.calendula.ui.common.PickerSearchField
|
||||
import de.jeanlucmakiola.calendula.widget.countdown.CountdownMode
|
||||
import de.jeanlucmakiola.calendula.widget.countdown.CountdownOptions
|
||||
import de.jeanlucmakiola.floret.components.FullScreenPicker
|
||||
import de.jeanlucmakiola.floret.components.GroupedChoiceRow
|
||||
import de.jeanlucmakiola.floret.components.GroupedHint
|
||||
import de.jeanlucmakiola.floret.components.GroupedRow
|
||||
import de.jeanlucmakiola.floret.components.GroupedSectionHeader
|
||||
import de.jeanlucmakiola.floret.components.Position
|
||||
import de.jeanlucmakiola.floret.components.positionOf
|
||||
import de.jeanlucmakiola.floret.locale.currentLocale
|
||||
import de.jeanlucmakiola.floret.locale.localizedDateFormatter
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toJavaLocalDate
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
|
||||
/** What the countdown widget counts to (#439): the next event, or one picked from a searchable list. */
|
||||
@Composable
|
||||
internal fun CountdownOptionsSection(
|
||||
options: CountdownOptions,
|
||||
candidates: CountdownCandidates,
|
||||
onChange: (CountdownOptions) -> Unit,
|
||||
onLoadCandidates: () -> Unit,
|
||||
) {
|
||||
var picking by rememberSaveable { mutableStateOf(false) }
|
||||
GroupedSectionHeader(stringResource(R.string.widget_config_countdown))
|
||||
GroupedChoiceRow(
|
||||
title = stringResource(R.string.widget_config_countdown),
|
||||
options = CountdownMode.entries,
|
||||
selected = options.mode,
|
||||
label = {
|
||||
stringResource(
|
||||
when (it) {
|
||||
CountdownMode.NEXT -> R.string.widget_config_countdown_next
|
||||
CountdownMode.PINNED -> R.string.widget_config_countdown_pinned
|
||||
},
|
||||
)
|
||||
},
|
||||
onSelect = { mode ->
|
||||
onChange(options.copy(mode = mode))
|
||||
if (mode == CountdownMode.PINNED && options.eventId == null) picking = true
|
||||
},
|
||||
position = if (options.mode == CountdownMode.PINNED) Position.Top else Position.Alone,
|
||||
)
|
||||
when (options.mode) {
|
||||
CountdownMode.NEXT -> GroupedHint(stringResource(R.string.widget_config_countdown_next_hint), verticalPadding = 8.dp)
|
||||
CountdownMode.PINNED -> GroupedRow(
|
||||
title = stringResource(R.string.widget_config_countdown_event),
|
||||
summary = options.title ?: stringResource(R.string.widget_config_countdown_event_none),
|
||||
position = Position.Bottom,
|
||||
onClick = { picking = true },
|
||||
)
|
||||
}
|
||||
if (picking) {
|
||||
LaunchedEffect(Unit) { onLoadCandidates() }
|
||||
CountdownEventPicker(
|
||||
candidates = candidates,
|
||||
selectedEventId = options.eventId,
|
||||
onRetry = onLoadCandidates,
|
||||
onSelect = { candidate ->
|
||||
onChange(options.copy(mode = CountdownMode.PINNED, eventId = candidate.event.eventId, title = candidate.event.title))
|
||||
picking = false
|
||||
},
|
||||
onDismiss = { picking = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CountdownEventPicker(
|
||||
candidates: CountdownCandidates,
|
||||
selectedEventId: Long?,
|
||||
onRetry: () -> Unit,
|
||||
onSelect: (CountdownCandidate) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
var query by rememberSaveable { mutableStateOf("") }
|
||||
FullScreenPicker(
|
||||
title = stringResource(R.string.widget_config_countdown_event),
|
||||
onDismiss = onDismiss,
|
||||
predictiveBack = true,
|
||||
scrollable = false,
|
||||
) {
|
||||
PickerSearchField(
|
||||
query = query,
|
||||
onQueryChange = { query = it },
|
||||
placeholder = stringResource(R.string.widget_config_countdown_search),
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
)
|
||||
when (candidates) {
|
||||
CountdownCandidates.Idle, CountdownCandidates.Loading -> Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) { CircularProgressIndicator() }
|
||||
CountdownCandidates.Failed -> Column(
|
||||
modifier = Modifier.fillMaxSize().padding(24.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
PickerMessage(stringResource(R.string.widget_config_countdown_load_failed))
|
||||
FilledTonalButton(onClick = onRetry, modifier = Modifier.padding(top = 16.dp)) {
|
||||
Text(stringResource(R.string.state_retry))
|
||||
}
|
||||
}
|
||||
is CountdownCandidates.Ready -> {
|
||||
val locale = currentLocale()
|
||||
val matches = remember(candidates, query) {
|
||||
val needle = query.trim()
|
||||
if (needle.isEmpty()) candidates.events
|
||||
else candidates.events.filter { it.event.title.contains(needle, ignoreCase = true) }
|
||||
}
|
||||
if (matches.isEmpty()) {
|
||||
PickerMessage(
|
||||
stringResource(R.string.widget_config_countdown_empty),
|
||||
Modifier.fillMaxWidth().padding(24.dp),
|
||||
)
|
||||
} else {
|
||||
val zone = remember { TimeZone.currentSystemDefault() }
|
||||
val formatter = remember(locale) { localizedDateFormatter(locale, "EEEdMMMy") }
|
||||
LazyColumn(contentPadding = PaddingValues(top = 8.dp, bottom = 24.dp)) {
|
||||
itemsIndexed(matches, key = { _, it -> it.event.eventId }) { index, candidate ->
|
||||
val date = formatter.format(candidate.event.start.toLocalDateTime(zone).date.toJavaLocalDate())
|
||||
GroupedRow(
|
||||
title = candidate.event.title.ifBlank { stringResource(R.string.event_untitled) },
|
||||
summary = if (candidate.repeats) stringResource(R.string.widget_config_countdown_repeats, date) else date,
|
||||
position = positionOf(index, matches.size),
|
||||
selected = candidate.event.eventId == selectedEventId,
|
||||
minHeight = 56.dp,
|
||||
leading = { CalendarColorChip(candidate.event.color) },
|
||||
onClick = { onSelect(candidate) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PickerMessage(text: String, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import androidx.glance.appwidget.GlanceAppWidgetManager
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetAppearance
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetKind
|
||||
import de.jeanlucmakiola.calendula.widget.widgetAppearance
|
||||
import de.jeanlucmakiola.calendula.widget.widgetState
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package de.jeanlucmakiola.calendula.widget.config
|
||||
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.jeanlucmakiola.calendula.R
|
||||
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip
|
||||
import de.jeanlucmakiola.calendula.ui.common.accountGroupTitle
|
||||
import de.jeanlucmakiola.calendula.ui.common.groupByAccount
|
||||
import de.jeanlucmakiola.calendula.ui.common.orUnnamed
|
||||
import de.jeanlucmakiola.floret.components.FullScreenPicker
|
||||
import de.jeanlucmakiola.floret.components.GroupedHint
|
||||
import de.jeanlucmakiola.floret.components.GroupedRow
|
||||
import de.jeanlucmakiola.floret.components.GroupedSectionHeader
|
||||
import de.jeanlucmakiola.floret.components.positionOf
|
||||
|
||||
/** Which of the app's calendars one widget shows, grouped by account like the drawer's filter. */
|
||||
@Composable
|
||||
internal fun WidgetCalendarPicker(
|
||||
calendars: List<CalendarSource>,
|
||||
hidden: Set<Long>,
|
||||
onSetShown: (Long, Boolean) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
FullScreenPicker(
|
||||
title = stringResource(R.string.widget_config_calendars),
|
||||
onDismiss = onDismiss,
|
||||
predictiveBack = true,
|
||||
) {
|
||||
GroupedHint(stringResource(R.string.widget_config_calendars_hint), verticalPadding = 8.dp)
|
||||
calendars.groupByAccount().forEach { group ->
|
||||
GroupedSectionHeader(accountGroupTitle(group))
|
||||
group.calendars.forEachIndexed { index, calendar ->
|
||||
val shown = calendar.id !in hidden
|
||||
GroupedRow(
|
||||
title = calendar.displayName.orUnnamed(),
|
||||
position = positionOf(index, group.calendars.size),
|
||||
minHeight = 56.dp,
|
||||
leading = { CalendarColorChip(calendar.color) },
|
||||
trailing = { Checkbox(checked = shown, onCheckedChange = { onSetShown(calendar.id, it) }) },
|
||||
onClick = { onSetShown(calendar.id, !shown) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** "All calendars", or how many of them the widget shows. */
|
||||
@Composable
|
||||
internal fun calendarsSummary(calendars: List<CalendarSource>, hidden: Set<Long>): String {
|
||||
val shown = calendars.count { it.id !in hidden }
|
||||
return if (shown == calendars.size) {
|
||||
stringResource(R.string.widget_config_calendars_all)
|
||||
} else {
|
||||
pluralStringResource(R.plurals.widget_config_calendars_some, calendars.size, shown, calendars.size)
|
||||
}
|
||||
}
|
||||
@@ -113,7 +113,10 @@ class WidgetConfigActivity : ComponentActivity() {
|
||||
onSave = viewModel::save,
|
||||
onRetry = viewModel::retry,
|
||||
onAppearance = viewModel::setAppearance,
|
||||
onAgendaOptions = viewModel::setAgendaOptions,
|
||||
onContent = viewModel::setContent,
|
||||
onCalendarShown = viewModel::setCalendarShown,
|
||||
onCountdown = viewModel::setCountdown,
|
||||
onLoadCountdownCandidates = viewModel::loadCountdownCandidates,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+105
-37
@@ -42,11 +42,21 @@ import de.jeanlucmakiola.calendula.ui.common.agendaRangeLabel
|
||||
import de.jeanlucmakiola.calendula.ui.settings.pastEventDisplayLabelRes
|
||||
import de.jeanlucmakiola.calendula.widget.AgendaWidgetOptions
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetAppearance
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetOption
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetSize
|
||||
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.calendula.widget.date.DateWidgetContent
|
||||
import de.jeanlucmakiola.calendula.widget.week.WeekStripWidgetContent
|
||||
import de.jeanlucmakiola.calendula.widget.upcoming.UpcomingWidgetContent
|
||||
import de.jeanlucmakiola.calendula.widget.today.TodayWidgetContent
|
||||
import de.jeanlucmakiola.calendula.widget.countdown.CountdownOptions
|
||||
import de.jeanlucmakiola.calendula.widget.countdown.CountdownWidgetContent
|
||||
import de.jeanlucmakiola.calendula.widget.split.MonthDayWidgetContent
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetKind
|
||||
import de.jeanlucmakiola.floret.components.GroupedChoiceRow
|
||||
import de.jeanlucmakiola.floret.components.GroupedHint
|
||||
import de.jeanlucmakiola.floret.components.GroupedRow
|
||||
@@ -54,6 +64,7 @@ import de.jeanlucmakiola.floret.components.GroupedSectionHeader
|
||||
import de.jeanlucmakiola.floret.components.GroupedSwitchRow
|
||||
import de.jeanlucmakiola.floret.components.OptionPicker
|
||||
import de.jeanlucmakiola.floret.components.Position
|
||||
import de.jeanlucmakiola.floret.components.positionOf
|
||||
import de.jeanlucmakiola.floret.glance.GlanceWidgetPreview
|
||||
import de.jeanlucmakiola.floret.glance.WallpaperWindow
|
||||
import de.jeanlucmakiola.floret.glance.WidgetBackground
|
||||
@@ -69,7 +80,10 @@ internal fun WidgetConfigScreen(
|
||||
onSave: () -> Unit,
|
||||
onRetry: () -> Unit,
|
||||
onAppearance: (WidgetAppearance) -> Unit,
|
||||
onAgendaOptions: (AgendaWidgetOptions) -> Unit,
|
||||
onContent: (AgendaWidgetOptions) -> Unit,
|
||||
onCalendarShown: (Long, Boolean) -> Unit,
|
||||
onCountdown: (CountdownOptions) -> Unit,
|
||||
onLoadCountdownCandidates: () -> Unit,
|
||||
) {
|
||||
val ready = state as? WidgetConfigUiState.Ready
|
||||
val surface = MaterialTheme.colorScheme.surface
|
||||
@@ -143,6 +157,27 @@ 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.MonthDay -> MonthDayWidgetContent(
|
||||
preview.data,
|
||||
preview.index,
|
||||
selected = null,
|
||||
appearance = state.appearance,
|
||||
appTheme = state.appTheme,
|
||||
preview = true,
|
||||
)
|
||||
is WidgetPreviewData.Countdown ->
|
||||
CountdownWidgetContent(preview.data, state.appearance, state.appTheme)
|
||||
is WidgetPreviewData.Days -> when (state.kind) {
|
||||
WidgetKind.TODAY ->
|
||||
TodayWidgetContent(preview.data, state.appearance, state.appTheme, preview = true)
|
||||
WidgetKind.UPCOMING -> UpcomingWidgetContent(preview.data, state.appearance, state.appTheme)
|
||||
else -> WeekStripWidgetContent(preview.data, state.appearance, state.appTheme)
|
||||
}
|
||||
is WidgetPreviewData.Today -> if (state.kind == WidgetKind.DATE) {
|
||||
DateWidgetContent(preview.today, state.appearance, state.appTheme)
|
||||
} else {
|
||||
QuickAddWidgetContent(preview.today, state.appearance, state.appTheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,7 +185,10 @@ internal fun WidgetConfigScreen(
|
||||
ConfigOptions(
|
||||
state = state,
|
||||
onAppearance = onAppearance,
|
||||
onAgendaOptions = onAgendaOptions,
|
||||
onContent = onContent,
|
||||
onCalendarShown = onCalendarShown,
|
||||
onCountdown = onCountdown,
|
||||
onLoadCountdownCandidates = onLoadCountdownCandidates,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.background(surface),
|
||||
@@ -173,17 +211,14 @@ private fun PreviewFailed() {
|
||||
)
|
||||
}
|
||||
|
||||
internal val WidgetKind.label: Int
|
||||
get() = when (this) {
|
||||
WidgetKind.AGENDA -> R.string.widget_agenda_label
|
||||
WidgetKind.MONTH -> R.string.widget_month_label
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConfigOptions(
|
||||
state: WidgetConfigUiState.Ready,
|
||||
onAppearance: (WidgetAppearance) -> Unit,
|
||||
onAgendaOptions: (AgendaWidgetOptions) -> Unit,
|
||||
onContent: (AgendaWidgetOptions) -> Unit,
|
||||
onCalendarShown: (Long, Boolean) -> Unit,
|
||||
onCountdown: (CountdownOptions) -> Unit,
|
||||
onLoadCountdownCandidates: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
@@ -209,47 +244,80 @@ private fun ConfigOptions(
|
||||
onSelect = { onAppearance(state.appearance.copy(theme = it)) },
|
||||
position = Position.Bottom,
|
||||
)
|
||||
state.agenda?.let { options ->
|
||||
AgendaOptions(options = options, state = state, onChange = onAgendaOptions)
|
||||
if (state.kind == WidgetKind.COUNTDOWN) {
|
||||
CountdownOptionsSection(
|
||||
options = state.draft.countdown,
|
||||
candidates = state.candidates,
|
||||
onChange = onCountdown,
|
||||
onLoadCandidates = onLoadCountdownCandidates,
|
||||
)
|
||||
}
|
||||
if (state.kind.options.isNotEmpty()) {
|
||||
ContentOptions(state = state, onChange = onContent, onCalendarShown = onCalendarShown)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AgendaOptions(
|
||||
options: AgendaWidgetOptions,
|
||||
private fun ContentOptions(
|
||||
state: WidgetConfigUiState.Ready,
|
||||
onChange: (AgendaWidgetOptions) -> Unit,
|
||||
onCalendarShown: (Long, Boolean) -> Unit,
|
||||
) {
|
||||
val options = state.draft.content
|
||||
var showRange by rememberSaveable { mutableStateOf(false) }
|
||||
var showSize by rememberSaveable { mutableStateOf(false) }
|
||||
var showPast by rememberSaveable { mutableStateOf(false) }
|
||||
var showCalendars by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
// Rows in WidgetOption order, each shaped by where it lands among the kind's own.
|
||||
val rows = WidgetOption.entries.filter { it in state.kind.options }
|
||||
fun position(option: WidgetOption) = positionOf(rows.indexOf(option), rows.size)
|
||||
|
||||
GroupedSectionHeader(stringResource(R.string.widget_config_content))
|
||||
GroupedRow(
|
||||
title = stringResource(R.string.widget_config_range),
|
||||
summary = agendaRangeLabel(options.range),
|
||||
position = Position.Top,
|
||||
onClick = { showRange = true },
|
||||
)
|
||||
GroupedRow(
|
||||
title = stringResource(R.string.widget_config_text_size),
|
||||
summary = widgetSizeLabel(options.textSize),
|
||||
position = Position.Middle,
|
||||
onClick = { showSize = true },
|
||||
)
|
||||
GroupedRow(
|
||||
title = stringResource(R.string.settings_past_events),
|
||||
summary = stringResource(pastEventDisplayLabelRes(options.pastDisplay)),
|
||||
position = Position.Middle,
|
||||
onClick = { showPast = true },
|
||||
)
|
||||
GroupedSwitchRow(
|
||||
title = stringResource(R.string.settings_agenda_show_today),
|
||||
checked = options.showToday,
|
||||
onCheckedChange = { onChange(options.copy(showToday = it)) },
|
||||
position = Position.Bottom,
|
||||
)
|
||||
rows.forEach { option ->
|
||||
when (option) {
|
||||
WidgetOption.RANGE -> GroupedRow(
|
||||
title = stringResource(R.string.widget_config_range),
|
||||
summary = agendaRangeLabel(options.range),
|
||||
position = position(option),
|
||||
onClick = { showRange = true },
|
||||
)
|
||||
WidgetOption.TEXT_SIZE -> GroupedRow(
|
||||
title = stringResource(R.string.widget_config_text_size),
|
||||
summary = widgetSizeLabel(options.textSize),
|
||||
position = position(option),
|
||||
onClick = { showSize = true },
|
||||
)
|
||||
WidgetOption.PAST_DISPLAY -> GroupedRow(
|
||||
title = stringResource(R.string.settings_past_events),
|
||||
summary = stringResource(pastEventDisplayLabelRes(options.pastDisplay)),
|
||||
position = position(option),
|
||||
onClick = { showPast = true },
|
||||
)
|
||||
WidgetOption.SHOW_TODAY -> GroupedSwitchRow(
|
||||
title = stringResource(R.string.settings_agenda_show_today),
|
||||
checked = options.showToday,
|
||||
onCheckedChange = { onChange(options.copy(showToday = it)) },
|
||||
position = position(option),
|
||||
)
|
||||
WidgetOption.CALENDARS -> GroupedRow(
|
||||
title = stringResource(R.string.widget_config_calendars),
|
||||
summary = calendarsSummary(state.calendars, state.draft.hiddenCalendars),
|
||||
position = position(option),
|
||||
onClick = { showCalendars = true },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showCalendars) {
|
||||
WidgetCalendarPicker(
|
||||
calendars = state.calendars,
|
||||
hidden = state.draft.hiddenCalendars,
|
||||
onSetShown = onCalendarShown,
|
||||
onDismiss = { showCalendars = false },
|
||||
)
|
||||
}
|
||||
|
||||
if (showRange) {
|
||||
AgendaRangePicker(
|
||||
|
||||
+140
-47
@@ -4,25 +4,44 @@ import android.appwidget.AppWidgetManager
|
||||
import android.content.Context
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.GlanceAppWidgetManager
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import de.jeanlucmakiola.calendula.data.calendar.CalendarRepository
|
||||
import de.jeanlucmakiola.calendula.data.prefs.CalendarPrefs
|
||||
import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
|
||||
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetKind
|
||||
import de.jeanlucmakiola.calendula.widget.DaysWidgetData
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetLoadContext
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetSettings
|
||||
import de.jeanlucmakiola.calendula.widget.loadDays
|
||||
import de.jeanlucmakiola.calendula.widget.weekOf
|
||||
import de.jeanlucmakiola.calendula.widget.upcoming.loadUpcoming
|
||||
import de.jeanlucmakiola.calendula.widget.today.loadTodayList
|
||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||
import de.jeanlucmakiola.calendula.widget.countdown.CountdownData
|
||||
import de.jeanlucmakiola.calendula.widget.countdown.CountdownOptions
|
||||
import de.jeanlucmakiola.calendula.widget.countdown.countdownOptions
|
||||
import de.jeanlucmakiola.calendula.widget.countdown.loadCountdown
|
||||
import de.jeanlucmakiola.calendula.widget.countdown.putCountdownOptions
|
||||
import de.jeanlucmakiola.calendula.widget.split.MonthDayData
|
||||
import de.jeanlucmakiola.calendula.widget.split.loadMonthDay
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetOption
|
||||
import de.jeanlucmakiola.calendula.widget.hiddenCalendars
|
||||
import de.jeanlucmakiola.calendula.widget.putHiddenCalendars
|
||||
import de.jeanlucmakiola.calendula.data.prefs.resolveFirstDay
|
||||
import de.jeanlucmakiola.calendula.widget.AgendaWidgetData
|
||||
import de.jeanlucmakiola.calendula.widget.AgendaWidgetOptions
|
||||
import de.jeanlucmakiola.calendula.widget.MonthWidgetSource
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetAppearance
|
||||
import de.jeanlucmakiola.calendula.widget.agenda.AgendaWidget
|
||||
import de.jeanlucmakiola.calendula.widget.agenda.AgendaWidgetReceiver
|
||||
import de.jeanlucmakiola.calendula.widget.loadAgendaWidgetData
|
||||
import de.jeanlucmakiola.calendula.widget.loadThemedMonthSource
|
||||
import de.jeanlucmakiola.calendula.widget.month.MonthWidget
|
||||
import de.jeanlucmakiola.calendula.widget.month.MonthWidgetReceiver
|
||||
import de.jeanlucmakiola.calendula.widget.monthIndexOf
|
||||
import de.jeanlucmakiola.calendula.widget.monthWindowCenter
|
||||
import de.jeanlucmakiola.calendula.widget.agendaWidgetOptions
|
||||
@@ -48,33 +67,41 @@ 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
|
||||
|
||||
/** The widgets a config screen can edit, told apart by their receiver. */
|
||||
enum class WidgetKind(val receiver: Class<*>) {
|
||||
AGENDA(AgendaWidgetReceiver::class.java),
|
||||
MONTH(MonthWidgetReceiver::class.java),
|
||||
;
|
||||
|
||||
fun widget(): GlanceAppWidget = when (this) {
|
||||
AGENDA -> AgendaWidget()
|
||||
MONTH -> MonthWidget()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun of(context: Context, appWidgetId: Int): WidgetKind? {
|
||||
val provider = AppWidgetManager.getInstance(context)?.getAppWidgetInfo(appWidgetId)?.provider ?: return null
|
||||
return entries.firstOrNull { it.receiver.name == provider.className }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 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
|
||||
data class Days(val data: DaysWidgetData) : WidgetPreviewData
|
||||
data class Countdown(val data: CountdownData) : WidgetPreviewData
|
||||
data class MonthDay(val data: MonthDayData, val index: Int) : WidgetPreviewData
|
||||
}
|
||||
|
||||
/** The options being edited, applied to the preview as they change and written on save. */
|
||||
data class WidgetDraft(
|
||||
val appearance: WidgetAppearance,
|
||||
/** The content options; stored only for a kind that [WidgetKind.hasContentOptions]. */
|
||||
val content: AgendaWidgetOptions,
|
||||
val hiddenCalendars: Set<Long>,
|
||||
val countdown: CountdownOptions = CountdownOptions(),
|
||||
)
|
||||
|
||||
/** One event or series the countdown can be pinned to, at its next occurrence. */
|
||||
data class CountdownCandidate(val event: EventInstance, val repeats: Boolean)
|
||||
|
||||
/** The countdown's event list, loaded when its picker first opens. */
|
||||
sealed interface CountdownCandidates {
|
||||
data object Idle : CountdownCandidates
|
||||
data object Loading : CountdownCandidates
|
||||
data object Failed : CountdownCandidates
|
||||
data class Ready(val events: List<CountdownCandidate>) : CountdownCandidates
|
||||
}
|
||||
|
||||
private fun WidgetDraft.loadContext(settings: WidgetSettings) = WidgetLoadContext(settings, content, hiddenCalendars)
|
||||
|
||||
sealed interface WidgetConfigUiState {
|
||||
data object Loading : WidgetConfigUiState
|
||||
|
||||
@@ -86,19 +113,22 @@ sealed interface WidgetConfigUiState {
|
||||
|
||||
data class Ready(
|
||||
val kind: WidgetKind,
|
||||
val appearance: WidgetAppearance,
|
||||
/** Null for a widget with no content options. */
|
||||
val agenda: AgendaWidgetOptions?,
|
||||
val draft: WidgetDraft,
|
||||
/** The calendars the app shows, for the widget's own calendar choice. */
|
||||
val calendars: List<CalendarSource>,
|
||||
val appTheme: ThemeMode,
|
||||
val weekStart: DayOfWeek,
|
||||
/** The widget's size on the home screen, which the preview is laid out at. */
|
||||
val placedSize: DpSize,
|
||||
/** Null until the first preview load lands. */
|
||||
val preview: WidgetPreviewData?,
|
||||
val candidates: CountdownCandidates = CountdownCandidates.Idle,
|
||||
/** The first preview load failed, so there's nothing to show. */
|
||||
val previewFailed: Boolean = false,
|
||||
val saving: Boolean = false,
|
||||
) : WidgetConfigUiState
|
||||
) : WidgetConfigUiState {
|
||||
val appearance: WidgetAppearance get() = draft.appearance
|
||||
}
|
||||
}
|
||||
|
||||
/** One-time outcomes of a save, for the screen that's showing when they land. */
|
||||
@@ -113,6 +143,8 @@ enum class WidgetConfigEvent { SAVED, SAVE_FAILED }
|
||||
class WidgetConfigViewModel @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val prefs: SettingsPrefs,
|
||||
private val calendarPrefs: CalendarPrefs,
|
||||
private val repository: CalendarRepository,
|
||||
savedState: SavedStateHandle,
|
||||
) : ViewModel() {
|
||||
|
||||
@@ -147,8 +179,13 @@ class WidgetConfigViewModel @Inject constructor(
|
||||
val settings = prefs.widgetSettings.first()
|
||||
WidgetConfigUiState.Ready(
|
||||
kind = kind,
|
||||
appearance = stored.widgetAppearance,
|
||||
agenda = if (kind == WidgetKind.AGENDA) stored.agendaWidgetOptions(settings.agendaDefaults) else null,
|
||||
draft = WidgetDraft(
|
||||
appearance = stored.widgetAppearance,
|
||||
content = stored.agendaWidgetOptions(settings.agendaDefaults),
|
||||
hiddenCalendars = stored.hiddenCalendars,
|
||||
countdown = stored.countdownOptions,
|
||||
),
|
||||
calendars = if (WidgetOption.CALENDARS in kind.options) shownCalendars() else emptyList(),
|
||||
appTheme = settings.themeMode,
|
||||
weekStart = settings.weekStart.resolveFirstDay(Locale.getDefault()),
|
||||
placedSize = placedSize(kind),
|
||||
@@ -171,16 +208,56 @@ class WidgetConfigViewModel @Inject constructor(
|
||||
val width = options?.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) ?: 0
|
||||
val height = options?.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT) ?: 0
|
||||
if (width > 0 && height > 0) return DpSize(width.dp, height.dp)
|
||||
return when (kind) {
|
||||
WidgetKind.AGENDA -> DpSize(260.dp, 280.dp)
|
||||
WidgetKind.MONTH -> DpSize(320.dp, 340.dp)
|
||||
return kind.defaultSize
|
||||
}
|
||||
|
||||
/** The calendars the app itself shows; the ones hidden there never reach a widget anyway. */
|
||||
private suspend fun shownCalendars(): List<CalendarSource> {
|
||||
val hidden = calendarPrefs.hiddenCalendarIds.first()
|
||||
return repository.calendars().first().filter { it.isVisibleInSystem && it.id !in hidden }
|
||||
}
|
||||
|
||||
fun setAppearance(appearance: WidgetAppearance) = edit { it.copy(draft = it.draft.copy(appearance = appearance)) }
|
||||
|
||||
fun setContent(options: AgendaWidgetOptions) {
|
||||
edit { it.copy(draft = it.draft.copy(content = options)) }
|
||||
reloadPreview()
|
||||
}
|
||||
|
||||
fun setCountdown(options: CountdownOptions) {
|
||||
edit { it.copy(draft = it.draft.copy(countdown = options)) }
|
||||
reloadPreview()
|
||||
}
|
||||
|
||||
/** Loads the events the countdown can be pinned to, unless they're loaded or loading. */
|
||||
fun loadCountdownCandidates() {
|
||||
val ready = _state.value as? WidgetConfigUiState.Ready ?: return
|
||||
if (ready.candidates is CountdownCandidates.Loading || ready.candidates is CountdownCandidates.Ready) return
|
||||
edit { it.copy(candidates = CountdownCandidates.Loading) }
|
||||
viewModelScope.launch {
|
||||
val candidates = try {
|
||||
val now = Clock.System.now()
|
||||
val occurrences = repository.instances(now..now + 365.days).first().filter { it.end > now }
|
||||
val perEvent = occurrences.groupBy { it.eventId }
|
||||
CountdownCandidates.Ready(
|
||||
perEvent.values
|
||||
.map { CountdownCandidate(it.minBy { o -> o.start }, repeats = it.size > 1) }
|
||||
.sortedBy { it.event.start },
|
||||
)
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (_: Exception) {
|
||||
CountdownCandidates.Failed
|
||||
}
|
||||
edit { it.copy(candidates = candidates) }
|
||||
}
|
||||
}
|
||||
|
||||
fun setAppearance(appearance: WidgetAppearance) = edit { it.copy(appearance = appearance) }
|
||||
|
||||
fun setAgendaOptions(options: AgendaWidgetOptions) {
|
||||
edit { it.copy(agenda = options) }
|
||||
fun setCalendarShown(id: Long, shown: Boolean) {
|
||||
edit {
|
||||
val hidden = it.draft.hiddenCalendars
|
||||
it.copy(draft = it.draft.copy(hiddenCalendars = if (shown) hidden - id else hidden + id))
|
||||
}
|
||||
reloadPreview()
|
||||
}
|
||||
|
||||
@@ -205,14 +282,28 @@ class WidgetConfigViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadPreview(ready: WidgetConfigUiState.Ready): WidgetPreviewData = when (ready.kind) {
|
||||
WidgetKind.AGENDA -> {
|
||||
val settings = prefs.widgetSettings.first()
|
||||
WidgetPreviewData.Agenda(context.loadAgendaWidgetData(settings, requireNotNull(ready.agenda)))
|
||||
}
|
||||
WidgetKind.MONTH -> {
|
||||
val index = monthIndexOf(today(systemZone()))
|
||||
WidgetPreviewData.Month(context.loadThemedMonthSource(monthWindowCenter(index)).content, index)
|
||||
private suspend fun loadPreview(ready: WidgetConfigUiState.Ready): WidgetPreviewData {
|
||||
val draft = ready.draft
|
||||
val settings = prefs.widgetSettings.first()
|
||||
return when (ready.kind) {
|
||||
WidgetKind.AGENDA ->
|
||||
WidgetPreviewData.Agenda(context.loadAgendaWidgetData(settings, draft.content, draft.hiddenCalendars))
|
||||
WidgetKind.MONTH -> {
|
||||
val index = monthIndexOf(today(systemZone()))
|
||||
val source = context.loadThemedMonthSource(monthWindowCenter(index), draft.hiddenCalendars)
|
||||
WidgetPreviewData.Month(source.content, index)
|
||||
}
|
||||
WidgetKind.QUICK_ADD, WidgetKind.DATE -> WidgetPreviewData.Today(today(systemZone()))
|
||||
WidgetKind.UPCOMING -> WidgetPreviewData.Days(context.loadUpcoming(draft.loadContext(settings)).content)
|
||||
WidgetKind.TODAY -> WidgetPreviewData.Days(context.loadTodayList(draft.loadContext(settings)).content)
|
||||
WidgetKind.MONTH_DAY -> {
|
||||
val index = monthIndexOf(today(systemZone()))
|
||||
val source = context.loadThemedMonthSource(monthWindowCenter(index), draft.hiddenCalendars)
|
||||
WidgetPreviewData.MonthDay(context.loadMonthDay(source, draft.content).content, index)
|
||||
}
|
||||
WidgetKind.COUNTDOWN ->
|
||||
WidgetPreviewData.Countdown(context.loadCountdown(draft.loadContext(settings), draft.countdown, null).content)
|
||||
WidgetKind.WEEK_STRIP -> WidgetPreviewData.Days(context.loadDays(draft.loadContext(settings), ::weekOf).content)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,8 +316,10 @@ class WidgetConfigViewModel @Inject constructor(
|
||||
try {
|
||||
val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(appWidgetId)
|
||||
ready.kind.widget().updateWithRefresh(context, glanceId) { state ->
|
||||
state.putAppearance(ready.appearance)
|
||||
ready.agenda?.let { state.putAgendaOptions(it) }
|
||||
state.putAppearance(ready.draft.appearance)
|
||||
if (ready.kind.hasContentOptions) state.putAgendaOptions(ready.draft.content)
|
||||
if (WidgetOption.CALENDARS in ready.kind.options) state.putHiddenCalendars(ready.draft.hiddenCalendars)
|
||||
if (ready.kind == WidgetKind.COUNTDOWN) state.putCountdownOptions(ready.draft.countdown)
|
||||
}
|
||||
_events.send(WidgetConfigEvent.SAVED)
|
||||
} catch (e: CancellationException) {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package de.jeanlucmakiola.calendula.widget.countdown
|
||||
|
||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||
import de.jeanlucmakiola.calendula.domain.isDeclined
|
||||
import kotlinx.datetime.DateTimeUnit
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.atStartOfDayIn
|
||||
import kotlinx.datetime.daysUntil
|
||||
import kotlinx.datetime.plus
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Instant
|
||||
|
||||
/** What a countdown widget counts down to (#439). */
|
||||
enum class CountdownMode { NEXT, PINNED }
|
||||
|
||||
data class CountdownOptions(
|
||||
val mode: CountdownMode = CountdownMode.NEXT,
|
||||
/** The pinned event or series; a series follows its next occurrence. */
|
||||
val eventId: Long? = null,
|
||||
/** The pinned event's title when it was chosen, for the config screen. */
|
||||
val title: String? = null,
|
||||
)
|
||||
|
||||
enum class CountdownUnit { NOW, MINUTES, HOURS, DAYS }
|
||||
|
||||
/** "in 25 min", "ends in 3 h", "in 12 days": [running] counts to the end instead of the start. */
|
||||
data class CountdownLabel(val running: Boolean, val unit: CountdownUnit, val amount: Int)
|
||||
|
||||
private fun target(event: EventInstance, now: Instant): Instant = if (event.start <= now) event.end else event.start
|
||||
|
||||
/**
|
||||
* The label for [event] at [now]. Under a minute reads "now", under an hour
|
||||
* counts minutes (rounded up), under a day whole hours (rounded), and beyond
|
||||
* that calendar days in [zone], so "in 1 day" is always tomorrow.
|
||||
*/
|
||||
fun countdownLabel(event: EventInstance, now: Instant, zone: TimeZone): CountdownLabel {
|
||||
val running = event.start <= now
|
||||
val target = target(event, now)
|
||||
val delta = target - now
|
||||
return when {
|
||||
delta < 1.minutes -> CountdownLabel(running, CountdownUnit.NOW, 0)
|
||||
delta < 1.hours -> CountdownLabel(running, CountdownUnit.MINUTES, ceil(delta / 1.minutes).toInt())
|
||||
delta < 24.hours -> CountdownLabel(running, CountdownUnit.HOURS, (delta / 1.hours).roundToInt().coerceAtLeast(1))
|
||||
else -> {
|
||||
val days = now.toLocalDateTime(zone).date.daysUntil(target.toLocalDateTime(zone).date)
|
||||
CountdownLabel(running, CountdownUnit.DAYS, days.coerceAtLeast(1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When [countdownLabel] next reads differently, so the widget redraws then
|
||||
* and not before: every minute in the last hour, at each half hour within the
|
||||
* day, and at midnight or the 24-hour mark beyond that.
|
||||
*/
|
||||
fun nextLabelChange(event: EventInstance, now: Instant, zone: TimeZone): Instant {
|
||||
val target = target(event, now)
|
||||
val label = countdownLabel(event, now, zone)
|
||||
val lowerBound: Duration = when (label.unit) {
|
||||
CountdownUnit.NOW -> Duration.ZERO
|
||||
CountdownUnit.MINUTES -> (label.amount - 1).minutes
|
||||
CountdownUnit.HOURS -> maxOf((label.amount - 0.5).hours, 1.hours)
|
||||
CountdownUnit.DAYS -> {
|
||||
val midnight = now.toLocalDateTime(zone).date.plus(1, DateTimeUnit.DAY).atStartOfDayIn(zone)
|
||||
return minOf(midnight, target - 24.hours).coerceAtLeast(now + 1.minutes)
|
||||
}
|
||||
}
|
||||
return (target - lowerBound).coerceAtLeast(now + 1.minutes)
|
||||
}
|
||||
|
||||
/**
|
||||
* The next-event mode's pick: timed, not declined, not over; of those, the one
|
||||
* whose countdown changes soonest — a running event's end, else a start.
|
||||
*/
|
||||
fun pickNextEvent(instances: List<EventInstance>, now: Instant): EventInstance? =
|
||||
instances
|
||||
.filter { !it.isAllDay && !it.isDeclined && it.end > now }
|
||||
.minByOrNull { target(it, now) }
|
||||
|
||||
/** A pinned event's occurrence to count to: the running or next one. */
|
||||
fun pickPinnedOccurrence(occurrences: List<EventInstance>, now: Instant, zone: TimeZone): EventInstance? =
|
||||
occurrences.filter { it.counted(zone).end > now }.minByOrNull { it.start }
|
||||
|
||||
/**
|
||||
* The span a countdown counts over. An all-day event is stored on UTC
|
||||
* midnights; it starts and ends at local midnight of those dates instead.
|
||||
*/
|
||||
fun EventInstance.counted(zone: TimeZone): EventInstance = if (!isAllDay) this else copy(
|
||||
start = start.toLocalDateTime(TimeZone.UTC).date.atStartOfDayIn(zone),
|
||||
end = end.toLocalDateTime(TimeZone.UTC).date.atStartOfDayIn(zone),
|
||||
)
|
||||
@@ -0,0 +1,167 @@
|
||||
package de.jeanlucmakiola.calendula.widget.countdown
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.core.net.toUri
|
||||
import androidx.datastore.preferences.core.MutablePreferences
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.glance.GlanceId
|
||||
import androidx.glance.appwidget.GlanceAppWidgetManager
|
||||
import de.jeanlucmakiola.calendula.data.prefs.is24Hour
|
||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||
import de.jeanlucmakiola.calendula.widget.Themed
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetLoadContext
|
||||
import de.jeanlucmakiola.calendula.widget.hasCalendarPermission
|
||||
import de.jeanlucmakiola.calendula.widget.systemZone
|
||||
import de.jeanlucmakiola.calendula.widget.today
|
||||
import de.jeanlucmakiola.calendula.widget.widgetEntryPoint
|
||||
import de.jeanlucmakiola.calendula.widget.widgetLoadContext
|
||||
import de.jeanlucmakiola.calendula.widget.widgetState
|
||||
import de.jeanlucmakiola.calendula.widget.withoutCalendars
|
||||
import de.jeanlucmakiola.floret.android.goAsync
|
||||
import de.jeanlucmakiola.floret.glance.updateWithRefresh
|
||||
import de.jeanlucmakiola.floret.prefs.toEnum
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Instant
|
||||
|
||||
private val ModeKey = stringPreferencesKey("cfg_countdown_mode")
|
||||
private val EventKey = longPreferencesKey("cfg_countdown_event")
|
||||
private val TitleKey = stringPreferencesKey("cfg_countdown_title")
|
||||
|
||||
internal val Preferences.countdownOptions: CountdownOptions
|
||||
get() = CountdownOptions(this[ModeKey].toEnum(CountdownMode.NEXT), this[EventKey], this[TitleKey])
|
||||
|
||||
internal fun MutablePreferences.putCountdownOptions(options: CountdownOptions) {
|
||||
this[ModeKey] = options.mode.name
|
||||
if (options.eventId != null) this[EventKey] = options.eventId else remove(EventKey)
|
||||
if (options.title != null) this[TitleKey] = options.title else remove(TitleKey)
|
||||
}
|
||||
|
||||
/** How far ahead the next-event mode looks; past this it shows no event. */
|
||||
private val NextEventHorizon = 30.days
|
||||
|
||||
/** How far ahead a pinned event's next occurrence is looked for. */
|
||||
private val PinnedHorizon = 730.days
|
||||
|
||||
sealed interface CountdownData {
|
||||
data object NeedsPermission : CountdownData
|
||||
data class Ready(
|
||||
val options: CountdownOptions,
|
||||
/** The occurrence counted to; null when there is none to show. */
|
||||
val event: EventInstance?,
|
||||
val now: Instant,
|
||||
val today: LocalDate,
|
||||
val is24Hour: Boolean,
|
||||
val soften: Boolean,
|
||||
/** The placed widget, for opening its settings; null in a preview. */
|
||||
val appWidgetId: Int?,
|
||||
) : CountdownData
|
||||
}
|
||||
|
||||
internal suspend fun Context.loadCountdown(
|
||||
load: WidgetLoadContext,
|
||||
options: CountdownOptions,
|
||||
appWidgetId: Int?,
|
||||
): Themed<CountdownData> {
|
||||
val settings = load.settings
|
||||
if (!hasCalendarPermission()) return Themed(CountdownData.NeedsPermission, settings.themeMode)
|
||||
val now = Clock.System.now()
|
||||
val repository = widgetEntryPoint().calendarRepository()
|
||||
val event = when (options.mode) {
|
||||
CountdownMode.NEXT ->
|
||||
pickNextEvent(repository.instances(now..now + NextEventHorizon).first().withoutCalendars(load.hiddenCalendars), now)
|
||||
CountdownMode.PINNED -> options.eventId?.let { id ->
|
||||
pickPinnedOccurrence(repository.eventOccurrences(id, now - 1.days..now + PinnedHorizon), now, systemZone())
|
||||
}
|
||||
}
|
||||
return Themed(
|
||||
CountdownData.Ready(
|
||||
options = options,
|
||||
event = event,
|
||||
now = now,
|
||||
today = today(systemZone()),
|
||||
is24Hour = settings.timeFormat.is24Hour(android.text.format.DateFormat.is24HourFormat(this)),
|
||||
soften = settings.soften,
|
||||
appWidgetId = appWidgetId,
|
||||
),
|
||||
settings.themeMode,
|
||||
)
|
||||
}
|
||||
|
||||
/** [loadCountdown] for placed widget [id], arming its redraw for when the label next changes. */
|
||||
internal suspend fun Context.loadPlacedCountdown(id: GlanceId): Themed<CountdownData> {
|
||||
val appWidgetId = GlanceAppWidgetManager(this).getAppWidgetId(id)
|
||||
val loaded = loadCountdown(widgetLoadContext(id), widgetState(id).countdownOptions, appWidgetId)
|
||||
val event = (loaded.content as? CountdownData.Ready)?.event
|
||||
if (event == null) {
|
||||
CountdownTicks.cancel(this, appWidgetId)
|
||||
} else {
|
||||
val zone = systemZone()
|
||||
CountdownTicks.schedule(this, appWidgetId, nextLabelChange(event.counted(zone), (loaded.content as CountdownData.Ready).now, zone))
|
||||
}
|
||||
return loaded
|
||||
}
|
||||
|
||||
/**
|
||||
* One alarm per countdown widget, for the moment its label changes. Not a
|
||||
* wake-up alarm: with the screen off nobody sees the widget, so the redraw
|
||||
* waits for the device to wake. Exact where allowed (the app holds the exact
|
||||
* alarm permission for reminders), else within a minute.
|
||||
*/
|
||||
internal object CountdownTicks {
|
||||
private const val ACTION_TICK = "de.jeanlucmakiola.calendula.widget.COUNTDOWN_TICK"
|
||||
|
||||
fun schedule(context: Context, appWidgetId: Int, at: Instant) {
|
||||
val alarms = context.getSystemService(AlarmManager::class.java) ?: return
|
||||
val intent = pendingIntent(context, appWidgetId)
|
||||
val millis = at.toEpochMilliseconds()
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || alarms.canScheduleExactAlarms()) {
|
||||
alarms.setExact(AlarmManager.RTC, millis, intent)
|
||||
} else {
|
||||
alarms.setWindow(AlarmManager.RTC, millis, 60_000L, intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel(context: Context, appWidgetId: Int) {
|
||||
context.getSystemService(AlarmManager::class.java)?.cancel(pendingIntent(context, appWidgetId))
|
||||
}
|
||||
|
||||
private fun pendingIntent(context: Context, appWidgetId: Int): PendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
0,
|
||||
Intent(context, CountdownTickReceiver::class.java)
|
||||
.setAction(ACTION_TICK)
|
||||
// Keys the PendingIntent per widget.
|
||||
.setData("calendula://countdown/$appWidgetId".toUri())
|
||||
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
}
|
||||
|
||||
/** Reloads the countdown widget whose label just changed; the load arms the next tick. */
|
||||
class CountdownTickReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
|
||||
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return
|
||||
val appContext = context.applicationContext
|
||||
goAsync {
|
||||
try {
|
||||
val glanceId = GlanceAppWidgetManager(appContext).getGlanceIdBy(appWidgetId)
|
||||
CountdownWidget().updateWithRefresh(appContext, glanceId)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// The widget is gone.
|
||||
CountdownTicks.cancel(appContext, appWidgetId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package de.jeanlucmakiola.calendula.widget.countdown
|
||||
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetKind
|
||||
import de.jeanlucmakiola.calendula.widget.previewSizeOf
|
||||
import de.jeanlucmakiola.calendula.widget.provideSamplePreview
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.glance.GlanceId
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.GlanceTheme
|
||||
import androidx.glance.LocalContext
|
||||
import androidx.glance.action.Action
|
||||
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.cornerRadius
|
||||
import androidx.glance.appwidget.provideContent
|
||||
import androidx.glance.background
|
||||
import androidx.glance.currentState
|
||||
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.fillMaxHeight
|
||||
import androidx.glance.layout.fillMaxSize
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.layout.width
|
||||
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||
import androidx.glance.text.FontWeight
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextStyle
|
||||
import de.jeanlucmakiola.calendula.MainActivity
|
||||
import de.jeanlucmakiola.calendula.R
|
||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||
import de.jeanlucmakiola.calendula.ui.common.eventAccent
|
||||
import de.jeanlucmakiola.calendula.widget.CalendulaWidgetReceiver
|
||||
import de.jeanlucmakiola.calendula.widget.CalendulaWidgetRoot
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetAppearance
|
||||
import de.jeanlucmakiola.calendula.widget.config.WidgetConfigActivity
|
||||
import de.jeanlucmakiola.calendula.widget.formatWidgetTime
|
||||
import de.jeanlucmakiola.calendula.widget.relativeDayLabel
|
||||
import de.jeanlucmakiola.calendula.widget.systemZone
|
||||
import de.jeanlucmakiola.calendula.widget.widgetAppearance
|
||||
import de.jeanlucmakiola.calendula.widget.widgetCardColor
|
||||
import de.jeanlucmakiola.floret.glance.LocalWidgetBackground
|
||||
import de.jeanlucmakiola.floret.glance.WidgetBackground
|
||||
import de.jeanlucmakiola.floret.glance.WidgetData
|
||||
import de.jeanlucmakiola.floret.glance.WidgetMessage
|
||||
import de.jeanlucmakiola.floret.glance.currentRefreshStamp
|
||||
import de.jeanlucmakiola.floret.glance.refreshStamp
|
||||
import de.jeanlucmakiola.floret.glance.themedColor
|
||||
import de.jeanlucmakiola.floret.prefs.ThemeMode
|
||||
import kotlinx.datetime.DateTimeUnit
|
||||
import kotlinx.datetime.minus
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
|
||||
/**
|
||||
* Countdown (#439): to the next timed event, or to a chosen event or series
|
||||
* ("Vacation · in 12 days"). Redraws when its label changes, see
|
||||
* [nextLabelChange] and [CountdownTicks]. Tapping opens the event.
|
||||
*/
|
||||
class CountdownWidget : GlanceAppWidget() {
|
||||
|
||||
override val stateDefinition = PreferencesGlanceStateDefinition
|
||||
|
||||
override val sizeMode = SizeMode.Single
|
||||
|
||||
override val previewSizeMode = previewSizeOf(WidgetKind.COUNTDOWN)
|
||||
|
||||
override suspend fun providePreview(context: Context, widgetCategory: Int) =
|
||||
provideSamplePreview(context, WidgetKind.COUNTDOWN)
|
||||
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
val widgetData = WidgetData(context.loadPlacedCountdown(id), refreshStamp(context, id))
|
||||
provideContent {
|
||||
val loaded = widgetData.current(currentRefreshStamp()) { context.loadPlacedCountdown(id) }
|
||||
CountdownWidgetContent(loaded.content, currentState<Preferences>().widgetAppearance, loaded.appTheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CountdownWidgetReceiver : CalendulaWidgetReceiver() {
|
||||
override val glanceAppWidget: GlanceAppWidget = CountdownWidget()
|
||||
|
||||
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
|
||||
super.onDeleted(context, appWidgetIds)
|
||||
appWidgetIds.forEach { CountdownTicks.cancel(context, it) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun CountdownWidgetContent(data: CountdownData, appearance: WidgetAppearance, appTheme: ThemeMode) {
|
||||
CalendulaWidgetRoot(appearance, appTheme) {
|
||||
val context = LocalContext.current
|
||||
when (data) {
|
||||
CountdownData.NeedsPermission -> WidgetMessage(context.getString(R.string.widget_needs_permission))
|
||||
is CountdownData.Ready -> {
|
||||
val event = data.event
|
||||
when {
|
||||
event != null -> EventCountdown(event, data)
|
||||
data.options.mode == CountdownMode.PINNED && data.options.eventId == null ->
|
||||
Message(context.getString(R.string.widget_countdown_choose), configAction(context, data.appWidgetId))
|
||||
data.options.mode == CountdownMode.PINNED ->
|
||||
Message(context.getString(R.string.widget_countdown_over, data.options.title.orEmpty()), configAction(context, data.appWidgetId))
|
||||
else -> Message(
|
||||
context.getString(R.string.widget_upcoming_empty),
|
||||
actionStartActivity(MainActivity.openViewIntent(context, null)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun configAction(context: Context, appWidgetId: Int?): Action? =
|
||||
appWidgetId?.takeIf { it != AppWidgetManager.INVALID_APPWIDGET_ID }
|
||||
?.let { actionStartActivity(WidgetConfigActivity.intent(context, it)) }
|
||||
|
||||
@Composable
|
||||
private fun Message(text: String, action: Action?) {
|
||||
val modifier = if (action != null) GlanceModifier.clickable(action) else GlanceModifier
|
||||
WidgetMessage(text, modifier = modifier)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EventCountdown(event: EventInstance, data: CountdownData.Ready) {
|
||||
val context = LocalContext.current
|
||||
val label = countdownLabel(event.counted(systemZone()), data.now, systemZone())
|
||||
val open = actionStartActivity(
|
||||
MainActivity.openEventIntent(
|
||||
context = context,
|
||||
eventId = event.eventId,
|
||||
beginMillis = event.start.toEpochMilliseconds(),
|
||||
endMillis = event.end.toEpochMilliseconds(),
|
||||
source = CalendarView.Agenda,
|
||||
),
|
||||
)
|
||||
// Over a wallpaper the text sits on its own card, as every other widget's rows do.
|
||||
val card = if (LocalWidgetBackground.current == WidgetBackground.TRANSPARENT) {
|
||||
GlanceModifier.cornerRadius(16.dp).background(widgetCardColor())
|
||||
} else {
|
||||
GlanceModifier
|
||||
}
|
||||
Box(modifier = GlanceModifier.fillMaxSize().padding(6.dp)) {
|
||||
Row(
|
||||
modifier = card.fillMaxSize().clickable(open).padding(horizontal = 10.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(
|
||||
modifier = GlanceModifier
|
||||
.width(4.dp)
|
||||
.fillMaxHeight()
|
||||
.cornerRadius(2.dp)
|
||||
.background(
|
||||
themedColor(
|
||||
day = eventAccent(event.color, dark = false, data.soften),
|
||||
night = eventAccent(event.color, dark = true, data.soften),
|
||||
),
|
||||
),
|
||||
) {}
|
||||
Spacer(GlanceModifier.width(10.dp))
|
||||
Column(modifier = GlanceModifier.defaultWeight(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = event.title.ifBlank { context.getString(R.string.event_untitled) },
|
||||
maxLines = 1,
|
||||
style = TextStyle(color = GlanceTheme.colors.onSurface, fontSize = 14.sp, fontWeight = FontWeight.Medium),
|
||||
)
|
||||
Text(
|
||||
text = countdownText(context.resources, label),
|
||||
maxLines = 1,
|
||||
style = TextStyle(color = GlanceTheme.colors.primary, fontSize = 22.sp, fontWeight = FontWeight.Bold),
|
||||
)
|
||||
Text(
|
||||
text = whenText(context, event, data),
|
||||
maxLines = 1,
|
||||
style = TextStyle(color = GlanceTheme.colors.onSurfaceVariant, fontSize = 12.sp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun countdownText(res: Resources, label: CountdownLabel): String = when (label.unit) {
|
||||
CountdownUnit.NOW -> res.getString(if (label.running) R.string.widget_countdown_ending_now else R.string.widget_countdown_now)
|
||||
CountdownUnit.MINUTES -> res.getQuantityString(
|
||||
if (label.running) R.plurals.widget_countdown_ends_minutes else R.plurals.widget_countdown_in_minutes,
|
||||
label.amount,
|
||||
label.amount,
|
||||
)
|
||||
CountdownUnit.HOURS -> res.getQuantityString(
|
||||
if (label.running) R.plurals.widget_countdown_ends_hours else R.plurals.widget_countdown_in_hours,
|
||||
label.amount,
|
||||
label.amount,
|
||||
)
|
||||
CountdownUnit.DAYS -> when {
|
||||
label.amount == 1 && label.running -> res.getString(R.string.widget_countdown_ends_tomorrow)
|
||||
label.amount == 1 -> res.getString(R.string.widget_countdown_tomorrow)
|
||||
else -> res.getQuantityString(
|
||||
if (label.running) R.plurals.widget_countdown_ends_days else R.plurals.widget_countdown_in_days,
|
||||
label.amount,
|
||||
label.amount,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** When it starts, or ends once running: "Today · 14:00", "Fri · All day". */
|
||||
private fun whenText(context: Context, event: EventInstance, data: CountdownData.Ready): String {
|
||||
val counted = event.counted(systemZone())
|
||||
val running = counted.start <= data.now
|
||||
val at = if (running) counted.end else counted.start
|
||||
val date = at.toLocalDateTime(systemZone()).date
|
||||
val day = relativeDayLabel(context, date, data.today)
|
||||
return when {
|
||||
event.isAllDay && running -> context.getString(
|
||||
R.string.widget_countdown_until,
|
||||
relativeDayLabel(context, date.minus(1, DateTimeUnit.DAY), data.today),
|
||||
)
|
||||
event.isAllDay -> "$day · ${context.getString(R.string.event_detail_all_day)}"
|
||||
running -> context.getString(R.string.agenda_span_ends, "$day · ${formatWidgetTime(at, data.is24Hour)}")
|
||||
else -> "$day · ${formatWidgetTime(at, data.is24Hour)}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package de.jeanlucmakiola.calendula.widget.date
|
||||
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetKind
|
||||
import de.jeanlucmakiola.calendula.widget.previewSizeOf
|
||||
import de.jeanlucmakiola.calendula.widget.provideSamplePreview
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.glance.GlanceId
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.GlanceTheme
|
||||
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.Column
|
||||
import androidx.glance.layout.Spacer
|
||||
import androidx.glance.layout.fillMaxSize
|
||||
import androidx.glance.layout.height
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.semantics.contentDescription
|
||||
import androidx.glance.semantics.semantics
|
||||
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||
import androidx.glance.text.FontWeight
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextAlign
|
||||
import androidx.glance.text.TextStyle
|
||||
import de.jeanlucmakiola.calendula.MainActivity
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||
import de.jeanlucmakiola.calendula.widget.CalendulaWidgetReceiver
|
||||
import de.jeanlucmakiola.calendula.widget.CalendulaWidgetRoot
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetAppearance
|
||||
import de.jeanlucmakiola.calendula.widget.loadToday
|
||||
import de.jeanlucmakiola.calendula.widget.widgetAppearance
|
||||
import de.jeanlucmakiola.calendula.widget.widgetLocale
|
||||
import de.jeanlucmakiola.floret.glance.LocalWidgetBackground
|
||||
import de.jeanlucmakiola.floret.glance.WidgetBackground
|
||||
import de.jeanlucmakiola.floret.glance.WidgetData
|
||||
import de.jeanlucmakiola.floret.glance.WidgetShape
|
||||
import de.jeanlucmakiola.floret.glance.WidgetShapedBox
|
||||
import de.jeanlucmakiola.floret.glance.currentRefreshStamp
|
||||
import de.jeanlucmakiola.floret.glance.refreshStamp
|
||||
import de.jeanlucmakiola.floret.glance.wallpaperBacking
|
||||
import de.jeanlucmakiola.floret.locale.localizedDateFormatter
|
||||
import de.jeanlucmakiola.floret.prefs.ThemeMode
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.toJavaDayOfWeek
|
||||
import kotlinx.datetime.toJavaLocalDate
|
||||
import java.time.format.TextStyle as JavaTextStyle
|
||||
|
||||
/** 1×1 date (#376): today's weekday above its number on an expressive shape; opens today. */
|
||||
class DateWidget : GlanceAppWidget() {
|
||||
|
||||
override val stateDefinition = PreferencesGlanceStateDefinition
|
||||
|
||||
override val sizeMode = SizeMode.Exact
|
||||
|
||||
override val previewSizeMode = previewSizeOf(WidgetKind.DATE)
|
||||
|
||||
override suspend fun providePreview(context: Context, widgetCategory: Int) =
|
||||
provideSamplePreview(context, WidgetKind.DATE)
|
||||
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
val widgetData = WidgetData(context.loadToday(), refreshStamp(context, id))
|
||||
provideContent {
|
||||
val loaded = widgetData.current(currentRefreshStamp()) { context.loadToday() }
|
||||
DateWidgetContent(loaded.content, currentState<Preferences>().widgetAppearance, loaded.appTheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DateWidgetReceiver : CalendulaWidgetReceiver() {
|
||||
override val glanceAppWidget: GlanceAppWidget = DateWidget()
|
||||
}
|
||||
|
||||
private val WeekdayLine = 18.dp
|
||||
|
||||
@Composable
|
||||
internal fun DateWidgetContent(today: LocalDate, appearance: WidgetAppearance, appTheme: ThemeMode) {
|
||||
CalendulaWidgetRoot(appearance, appTheme) {
|
||||
val context = LocalContext.current
|
||||
val size = LocalSize.current
|
||||
val fontScale = context.resources.configuration.fontScale
|
||||
val side = (minOf(size.width, size.height - WeekdayLine * fontScale) - 8.dp).coerceIn(32.dp, 120.dp)
|
||||
val label = localizedDateFormatter(widgetLocale(), "EEEEdMMMM").format(today.toJavaLocalDate())
|
||||
val weekday = TextStyle(
|
||||
color = GlanceTheme.colors.onSurface,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Column(
|
||||
modifier = GlanceModifier
|
||||
.fillMaxSize()
|
||||
.clickable(actionStartActivity(MainActivity.openDateIntent(context, today, CalendarView.Day)))
|
||||
.semantics { contentDescription = label },
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val name = today.dayOfWeek.toJavaDayOfWeek().getDisplayName(JavaTextStyle.SHORT, widgetLocale())
|
||||
val backing = if (LocalWidgetBackground.current == WidgetBackground.TRANSPARENT) {
|
||||
GlanceModifier.wallpaperBacking(radius = 8.dp).padding(horizontal = 6.dp)
|
||||
} else {
|
||||
GlanceModifier
|
||||
}
|
||||
Text(text = name.uppercase(widgetLocale()), maxLines = 1, style = weekday, modifier = backing)
|
||||
Spacer(GlanceModifier.height(2.dp))
|
||||
WidgetShapedBox(shape = WidgetShape.COOKIE, color = GlanceTheme.colors.primary, size = side) {
|
||||
Text(
|
||||
text = today.day.toString(),
|
||||
maxLines = 1,
|
||||
style = TextStyle(
|
||||
color = GlanceTheme.colors.onPrimary,
|
||||
// Sized to the shape, so held against the font scale.
|
||||
fontSize = (side.value * 0.45f / fontScale).sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package de.jeanlucmakiola.calendula.widget.month
|
||||
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetKind
|
||||
import de.jeanlucmakiola.calendula.widget.previewSizeOf
|
||||
import de.jeanlucmakiola.calendula.widget.provideSamplePreview
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
@@ -43,7 +46,6 @@ 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.state.PreferencesGlanceStateDefinition
|
||||
import de.jeanlucmakiola.floret.glance.LocalWidgetThemeMode
|
||||
import de.jeanlucmakiola.floret.glance.WidgetColorRole
|
||||
@@ -136,6 +138,11 @@ class MonthWidget : GlanceAppWidget() {
|
||||
*/
|
||||
override val sizeMode = SizeMode.Exact
|
||||
|
||||
override val previewSizeMode = previewSizeOf(WidgetKind.MONTH)
|
||||
|
||||
override suspend fun providePreview(context: Context, widgetCategory: Int) =
|
||||
provideSamplePreview(context, WidgetKind.MONTH)
|
||||
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
val shown = getAppWidgetState<Preferences>(context, id)[MONTH_INDEX_KEY]
|
||||
?: currentMonthIndex(systemZone())
|
||||
@@ -239,8 +246,6 @@ private fun MonthWidgetBody(source: MonthWidgetSource, index: Int) {
|
||||
is MonthWidgetSource.Ready -> {
|
||||
val zone = systemZone()
|
||||
val ym = yearMonthOf(index)
|
||||
// Column width from the live widget size, minus our H padding.
|
||||
val colW = (LocalSize.current.width - GRID_HPADDING * 2) / 7
|
||||
val weeks = layoutMonthWeeks(ym, source.weekStart, source.instances, zone)
|
||||
val context = LocalContext.current
|
||||
val fontScale = context.resources.configuration.fontScale
|
||||
@@ -261,7 +266,7 @@ private fun MonthWidgetBody(source: MonthWidgetSource, index: Int) {
|
||||
// each carry their own fill, so a transparent widget stays
|
||||
// readable without one blob over the wallpaper (#292).
|
||||
Column(modifier = GlanceModifier.fillMaxWidth().defaultWeight()) {
|
||||
WeekdayHeader(weekStart = source.weekStart, colW = colW)
|
||||
WeekdayHeader(weekStart = source.weekStart)
|
||||
AndroidRemoteViews(
|
||||
remoteViews = monthGridViews(context, layouts, ym.month, source.today, source.soften, colors, mode),
|
||||
modifier = GlanceModifier.fillMaxWidth().defaultWeight(),
|
||||
@@ -315,10 +320,10 @@ private fun MonthHeader(label: String, today: LocalDate) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WeekdayHeader(weekStart: DayOfWeek, colW: Dp) {
|
||||
private fun WeekdayHeader(weekStart: DayOfWeek) {
|
||||
Row(modifier = GlanceModifier.fillMaxWidth().padding(bottom = 1.dp)) {
|
||||
weekdayNarrowNames(weekStart).forEach { name ->
|
||||
Box(modifier = GlanceModifier.width(colW).padding(horizontal = 1.dp)) {
|
||||
Box(modifier = GlanceModifier.defaultWeight().padding(horizontal = 1.dp)) {
|
||||
Text(
|
||||
text = name,
|
||||
style = TextStyle(
|
||||
|
||||
@@ -1,56 +1,9 @@
|
||||
package de.jeanlucmakiola.calendula.widget.month
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Context
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||
import de.jeanlucmakiola.calendula.widget.syncWidgetRollover
|
||||
import de.jeanlucmakiola.floret.glance.reloadOnUpdate
|
||||
import de.jeanlucmakiola.floret.glance.restoreState
|
||||
import de.jeanlucmakiola.calendula.widget.CalendulaWidgetReceiver
|
||||
|
||||
/**
|
||||
* Host-facing receiver for the month widget. Declared in the manifest with the
|
||||
* `appwidget_info_month` provider metadata; delegates rendering to [MonthWidget].
|
||||
*/
|
||||
class MonthWidgetReceiver : GlanceAppWidgetReceiver() {
|
||||
/** Host-facing receiver for the month widget; rendering is [MonthWidget]'s. */
|
||||
class MonthWidgetReceiver : CalendulaWidgetReceiver() {
|
||||
override val glanceAppWidget: GlanceAppWidget = MonthWidget()
|
||||
|
||||
/** First month widget placed — start rolling "today" over at midnight (#228). */
|
||||
override fun onEnabled(context: Context) {
|
||||
super.onEnabled(context)
|
||||
syncWidgetRollover(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Last month widget removed. [syncWidgetRollover] cancels only if
|
||||
* no agenda widget is left either.
|
||||
*/
|
||||
override fun onDisabled(context: Context) {
|
||||
super.onDisabled(context)
|
||||
syncWidgetRollover(context)
|
||||
}
|
||||
|
||||
/** A backup restore or device transfer renumbered the widgets: keep each one's settings (#292). */
|
||||
override fun onRestored(context: Context, oldWidgetIds: IntArray, newWidgetIds: IntArray) {
|
||||
restoreState(context, oldWidgetIds, newWidgetIds)
|
||||
}
|
||||
|
||||
/**
|
||||
* The `updatePeriodMillis` backstop is the one wake-up the *system* still
|
||||
* owns, so it doubles as the alarm's self-heal: anything that drops a
|
||||
* pending alarm without a broadcast (force-stop, battery restriction, an OEM
|
||||
* freeze) is repaired here rather than on the next app open. Re-arming
|
||||
* closer to midnight also narrows the inexact delivery window.
|
||||
*/
|
||||
@SuppressLint("MissingSuperCall")
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
) {
|
||||
// In place of super, which only recomposes and would take the goAsync handle.
|
||||
reloadOnUpdate(context, appWidgetIds)
|
||||
syncWidgetRollover(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package de.jeanlucmakiola.calendula.widget.quickadd
|
||||
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetKind
|
||||
import de.jeanlucmakiola.calendula.widget.previewSizeOf
|
||||
import de.jeanlucmakiola.calendula.widget.provideSamplePreview
|
||||
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 val previewSizeMode = previewSizeOf(WidgetKind.QUICK_ADD)
|
||||
|
||||
override suspend fun providePreview(context: Context, widgetCategory: Int) =
|
||||
provideSamplePreview(context, WidgetKind.QUICK_ADD)
|
||||
|
||||
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,451 @@
|
||||
package de.jeanlucmakiola.calendula.widget.split
|
||||
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetKind
|
||||
import de.jeanlucmakiola.calendula.widget.previewSizeOf
|
||||
import de.jeanlucmakiola.calendula.widget.provideSamplePreview
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
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.ActionParameters
|
||||
import androidx.glance.action.actionParametersOf
|
||||
import androidx.glance.action.clickable
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.SizeMode
|
||||
import androidx.glance.appwidget.action.ActionCallback
|
||||
import androidx.glance.appwidget.action.actionRunCallback
|
||||
import androidx.glance.appwidget.action.actionStartActivity
|
||||
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.appwidget.state.updateAppWidgetState
|
||||
import androidx.glance.background
|
||||
import androidx.glance.currentState
|
||||
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.fillMaxHeight
|
||||
import androidx.glance.layout.fillMaxSize
|
||||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.height
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.layout.width
|
||||
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||
import androidx.glance.text.FontWeight
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextAlign
|
||||
import androidx.glance.text.TextStyle
|
||||
import androidx.glance.unit.ColorProvider
|
||||
import de.jeanlucmakiola.calendula.MainActivity
|
||||
import de.jeanlucmakiola.calendula.R
|
||||
import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay
|
||||
import de.jeanlucmakiola.calendula.domain.hasEnded
|
||||
import de.jeanlucmakiola.calendula.ui.agenda.groupAgendaDays
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||
import de.jeanlucmakiola.calendula.ui.common.formatCalendarTitle
|
||||
import de.jeanlucmakiola.calendula.widget.AgendaWidgetOptions
|
||||
import de.jeanlucmakiola.calendula.widget.CalendulaWidgetReceiver
|
||||
import de.jeanlucmakiola.calendula.widget.CalendulaWidgetRoot
|
||||
import de.jeanlucmakiola.calendula.widget.MonthWidgetSource
|
||||
import de.jeanlucmakiola.calendula.widget.Themed
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetAppearance
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetEventRow
|
||||
import de.jeanlucmakiola.calendula.widget.widgetLoadContext
|
||||
import de.jeanlucmakiola.calendula.widget.compactRowMetrics
|
||||
import de.jeanlucmakiola.calendula.widget.loadCachedMonthSource
|
||||
import de.jeanlucmakiola.calendula.widget.monthIndexOf
|
||||
import de.jeanlucmakiola.calendula.widget.monthWindowCenter
|
||||
import de.jeanlucmakiola.calendula.widget.relativeDayLabel
|
||||
import de.jeanlucmakiola.calendula.widget.systemZone
|
||||
import de.jeanlucmakiola.calendula.widget.today
|
||||
import de.jeanlucmakiola.calendula.widget.weekOf
|
||||
import de.jeanlucmakiola.calendula.widget.widgetAppearance
|
||||
import de.jeanlucmakiola.calendula.widget.widgetLocale
|
||||
import de.jeanlucmakiola.calendula.widget.widgetCardColor
|
||||
import de.jeanlucmakiola.calendula.widget.widgetEntryPoint
|
||||
import de.jeanlucmakiola.calendula.widget.widgetSettings
|
||||
import de.jeanlucmakiola.calendula.widget.widgetState
|
||||
import de.jeanlucmakiola.floret.locale.localizedDateFormatter
|
||||
import kotlinx.datetime.toJavaLocalDate
|
||||
import de.jeanlucmakiola.calendula.widget.widgetTimeLabel
|
||||
import de.jeanlucmakiola.calendula.data.prefs.is24Hour
|
||||
import de.jeanlucmakiola.floret.components.positionOf
|
||||
import de.jeanlucmakiola.floret.glance.LocalWidgetBackground
|
||||
import de.jeanlucmakiola.floret.glance.WidgetActionStyle
|
||||
import de.jeanlucmakiola.floret.glance.WidgetBackground
|
||||
import de.jeanlucmakiola.floret.glance.WidgetData
|
||||
import de.jeanlucmakiola.floret.glance.WidgetHeader
|
||||
import de.jeanlucmakiola.floret.glance.WidgetHeaderAction
|
||||
import de.jeanlucmakiola.floret.glance.WidgetHeaderActionGap
|
||||
import de.jeanlucmakiola.floret.glance.WidgetHeaderTextAction
|
||||
import de.jeanlucmakiola.floret.glance.WidgetMessage
|
||||
import de.jeanlucmakiola.floret.glance.currentRefreshStamp
|
||||
import de.jeanlucmakiola.floret.glance.refreshStamp
|
||||
import de.jeanlucmakiola.floret.glance.wallpaperBacking
|
||||
import de.jeanlucmakiola.floret.prefs.ThemeMode
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.datetime.DateTimeUnit
|
||||
import kotlinx.datetime.DayOfWeek
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.plus
|
||||
import kotlinx.datetime.toJavaDayOfWeek
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
import java.time.format.TextStyle as JavaTextStyle
|
||||
|
||||
/** Per-widget state: the shown month (`year * 12 + monthOrdinal`) and the selected day; both follow today when absent. */
|
||||
private val MONTH_INDEX_KEY = intPreferencesKey("split_month_index")
|
||||
private val SELECTED_KEY = stringPreferencesKey("split_selected")
|
||||
|
||||
/** What the month + day widget draws: the month widget's window, plus what its list needs. */
|
||||
sealed interface MonthDayData {
|
||||
data object NeedsPermission : MonthDayData
|
||||
data class Ready(
|
||||
val source: MonthWidgetSource.Ready,
|
||||
val options: AgendaWidgetOptions,
|
||||
val is24Hour: Boolean,
|
||||
val now: Instant,
|
||||
) : MonthDayData
|
||||
}
|
||||
|
||||
/** [loaded]'s month window with what the day list draws by. */
|
||||
internal suspend fun Context.loadMonthDay(loaded: Themed<MonthWidgetSource>, options: AgendaWidgetOptions): Themed<MonthDayData> {
|
||||
val ready = loaded.content as? MonthWidgetSource.Ready ?: return Themed(MonthDayData.NeedsPermission, loaded.appTheme)
|
||||
val settings = widgetEntryPoint().settingsPrefs().widgetSettings.first()
|
||||
return Themed(
|
||||
MonthDayData.Ready(
|
||||
source = ready,
|
||||
options = options,
|
||||
is24Hour = settings.timeFormat.is24Hour(android.text.format.DateFormat.is24HourFormat(this)),
|
||||
now = Clock.System.now(),
|
||||
),
|
||||
loaded.appTheme,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Month + day (#441): a compact month grid beside the selected day's events,
|
||||
* like Samsung Calendar's. A day in the grid selects it, starting on today;
|
||||
* the list and its events open the app. The plain month widget keeps
|
||||
* tap-opens-the-day (#386).
|
||||
*/
|
||||
class MonthDayWidget : GlanceAppWidget() {
|
||||
|
||||
override val stateDefinition = PreferencesGlanceStateDefinition
|
||||
|
||||
override val sizeMode = SizeMode.Exact
|
||||
|
||||
override val previewSizeMode = previewSizeOf(WidgetKind.MONTH_DAY)
|
||||
|
||||
override suspend fun providePreview(context: Context, widgetCategory: Int) =
|
||||
provideSamplePreview(context, WidgetKind.MONTH_DAY)
|
||||
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
val stamp = refreshStamp(context, id)
|
||||
val shown = context.widgetState(id)[MONTH_INDEX_KEY] ?: monthIndexOf(today(systemZone()))
|
||||
val center = monthWindowCenter(shown)
|
||||
val load: suspend (Int, Long) -> Themed<MonthDayData> = { c, s ->
|
||||
context.loadMonthDay(context.loadCachedMonthSource(id, c, s), context.widgetLoadContext(id).options)
|
||||
}
|
||||
val widgetData = WidgetData(load(center, stamp), center, stamp)
|
||||
provideContent {
|
||||
val index = currentState(MONTH_INDEX_KEY) ?: monthIndexOf(today(systemZone()))
|
||||
val currentCenter = monthWindowCenter(index)
|
||||
val currentStamp = currentRefreshStamp()
|
||||
val loaded = widgetData.current(currentCenter, currentStamp) { load(currentCenter, currentStamp) }
|
||||
val selected = currentState(SELECTED_KEY)?.let(LocalDate::parse)
|
||||
MonthDayWidgetContent(loaded.content, index, selected, currentState<Preferences>().widgetAppearance, loaded.appTheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MonthDayWidgetReceiver : CalendulaWidgetReceiver() {
|
||||
override val glanceAppWidget: GlanceAppWidget = MonthDayWidget()
|
||||
}
|
||||
|
||||
/** Selects the tapped day; the list redraws from what's loaded. */
|
||||
class SelectDayAction : ActionCallback {
|
||||
override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
|
||||
val date = parameters[dateKey] ?: return
|
||||
updateAppWidgetState(context, glanceId) { prefs ->
|
||||
if (date == today(systemZone()).toString()) prefs.remove(SELECTED_KEY) else prefs[SELECTED_KEY] = date
|
||||
}
|
||||
MonthDayWidget().update(context.applicationContext, glanceId)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val dateKey = ActionParameters.Key<String>("date")
|
||||
}
|
||||
}
|
||||
|
||||
/** Pages the grid by the `delta` parameter; landing on the current month follows the date again. */
|
||||
class ShiftMonthDayAction : ActionCallback {
|
||||
override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
|
||||
val delta = parameters[deltaKey] ?: 0
|
||||
val current = monthIndexOf(today(systemZone()))
|
||||
updateAppWidgetState(context, glanceId) { prefs ->
|
||||
val next = (prefs[MONTH_INDEX_KEY] ?: current) + delta
|
||||
if (next == current) prefs.remove(MONTH_INDEX_KEY) else prefs[MONTH_INDEX_KEY] = next
|
||||
}
|
||||
MonthDayWidget().update(context.applicationContext, glanceId)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val deltaKey = ActionParameters.Key<Int>("delta")
|
||||
}
|
||||
}
|
||||
|
||||
/** Back to this month with today selected. */
|
||||
class ResetMonthDayAction : ActionCallback {
|
||||
override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
|
||||
updateAppWidgetState(context, glanceId) { prefs ->
|
||||
prefs.remove(MONTH_INDEX_KEY)
|
||||
prefs.remove(SELECTED_KEY)
|
||||
}
|
||||
MonthDayWidget().update(context.applicationContext, glanceId)
|
||||
}
|
||||
}
|
||||
|
||||
/** The grid's days: whole weeks from [weekStart] covering [index]'s month. */
|
||||
internal fun monthGridDays(index: Int, weekStart: DayOfWeek): List<LocalDate> {
|
||||
val first = LocalDate(index / 12, index % 12 + 1, 1)
|
||||
val start = weekOf(first, weekStart).start
|
||||
val last = first.plus(1, DateTimeUnit.MONTH).plus(-1, DateTimeUnit.DAY)
|
||||
val weeks = (start.daysUntilInclusive(last) + 6) / 7
|
||||
return List(weeks * 7) { start.plus(it, DateTimeUnit.DAY) }
|
||||
}
|
||||
|
||||
private fun LocalDate.daysUntilInclusive(other: LocalDate): Int =
|
||||
(other.toEpochDays() - toEpochDays()).toInt() + 1
|
||||
|
||||
private val GridGap = 8.dp
|
||||
|
||||
@Composable
|
||||
internal fun MonthDayWidgetContent(
|
||||
data: MonthDayData,
|
||||
index: Int,
|
||||
selected: LocalDate?,
|
||||
appearance: WidgetAppearance,
|
||||
appTheme: ThemeMode,
|
||||
preview: Boolean = false,
|
||||
) {
|
||||
CalendulaWidgetRoot(appearance, appTheme) {
|
||||
val context = LocalContext.current
|
||||
when (data) {
|
||||
MonthDayData.NeedsPermission -> WidgetMessage(context.getString(R.string.widget_needs_permission))
|
||||
is MonthDayData.Ready -> {
|
||||
val source = data.source
|
||||
val day = selected ?: source.today
|
||||
val zone = systemZone()
|
||||
val days = monthGridDays(index, source.weekStart)
|
||||
val busy = groupAgendaDays(days.first(), days.last(), source.instances, zone).map { it.date }.toSet()
|
||||
Column(modifier = GlanceModifier.fillMaxSize().padding(horizontal = 8.dp, vertical = 6.dp)) {
|
||||
Header(index, source.today)
|
||||
Spacer(GlanceModifier.height(4.dp))
|
||||
Row(modifier = GlanceModifier.fillMaxWidth().defaultWeight()) {
|
||||
val gridWidth = (LocalSize.current.width - 16.dp - GridGap) * 0.52f
|
||||
Box(modifier = GlanceModifier.width(gridWidth).fillMaxHeight()) {
|
||||
Grid(days, index, source, day, busy, gridWidth)
|
||||
}
|
||||
Spacer(GlanceModifier.width(GridGap))
|
||||
Box(modifier = GlanceModifier.defaultWeight().fillMaxHeight()) {
|
||||
DayList(day, data, preview)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Header(index: Int, today: LocalDate) {
|
||||
val context = LocalContext.current
|
||||
WidgetHeader(
|
||||
title = formatCalendarTitle(
|
||||
date = java.time.LocalDate.of(index / 12, index % 12 + 1, 1),
|
||||
locale = widgetLocale(),
|
||||
currentYear = today.year,
|
||||
skeleton = "LLLL",
|
||||
),
|
||||
titleAction = actionStartActivity(MainActivity.openViewIntent(context, CalendarView.Month)),
|
||||
modifier = GlanceModifier.padding(bottom = 2.dp),
|
||||
titleInset = 4.dp,
|
||||
) {
|
||||
WidgetHeaderAction(
|
||||
icon = ImageProvider(R.drawable.ic_widget_chevron_left),
|
||||
contentDescription = context.getString(R.string.widget_prev_month),
|
||||
action = actionRunCallback<ShiftMonthDayAction>(actionParametersOf(ShiftMonthDayAction.deltaKey to -1)),
|
||||
style = WidgetActionStyle.TONAL,
|
||||
)
|
||||
WidgetHeaderActionGap()
|
||||
WidgetHeaderTextAction(
|
||||
text = today.day.toString(),
|
||||
contentDescription = context.getString(R.string.widget_today),
|
||||
action = actionRunCallback<ResetMonthDayAction>(),
|
||||
style = WidgetActionStyle.TONAL,
|
||||
)
|
||||
WidgetHeaderActionGap()
|
||||
WidgetHeaderAction(
|
||||
icon = ImageProvider(R.drawable.ic_widget_chevron_right),
|
||||
contentDescription = context.getString(R.string.widget_next_month),
|
||||
action = actionRunCallback<ShiftMonthDayAction>(actionParametersOf(ShiftMonthDayAction.deltaKey to 1)),
|
||||
style = WidgetActionStyle.TONAL,
|
||||
)
|
||||
WidgetHeaderActionGap()
|
||||
WidgetHeaderAction(
|
||||
icon = ImageProvider(R.drawable.ic_widget_add),
|
||||
contentDescription = context.getString(R.string.widget_new_event),
|
||||
action = actionStartActivity(MainActivity.openCreateIntent(context, today)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The month as numbers: selected day filled, today in bold primary, days
|
||||
* with events dotted, days around the month faded. Three views a day keeps
|
||||
* a six-week month well inside Glance's view ids (#372).
|
||||
*/
|
||||
@Composable
|
||||
private fun Grid(
|
||||
days: List<LocalDate>,
|
||||
index: Int,
|
||||
source: MonthWidgetSource.Ready,
|
||||
selected: LocalDate,
|
||||
busy: Set<LocalDate>,
|
||||
width: Dp,
|
||||
) {
|
||||
val locale = widgetLocale()
|
||||
val month = index % 12 + 1
|
||||
val backing = if (LocalWidgetBackground.current == WidgetBackground.TRANSPARENT) {
|
||||
GlanceModifier.cornerRadius(14.dp).background(widgetCardColor())
|
||||
} else {
|
||||
GlanceModifier
|
||||
}
|
||||
Column(modifier = backing.fillMaxSize().padding(2.dp)) {
|
||||
Row(modifier = GlanceModifier.fillMaxWidth()) {
|
||||
for (i in 0 until 7) {
|
||||
Text(
|
||||
text = java.time.DayOfWeek.of((source.weekStart.ordinal + i) % 7 + 1).getDisplayName(JavaTextStyle.NARROW, locale),
|
||||
style = TextStyle(color = GlanceTheme.colors.onSurfaceVariant, fontSize = 10.sp, textAlign = TextAlign.Center),
|
||||
modifier = GlanceModifier.width((width - 4.dp) / 7),
|
||||
)
|
||||
}
|
||||
}
|
||||
days.chunked(7).forEach { week ->
|
||||
Row(modifier = GlanceModifier.fillMaxWidth().defaultWeight(), verticalAlignment = Alignment.CenterVertically) {
|
||||
week.forEach { date ->
|
||||
DayCell(date, date.month.ordinal + 1 == month, date == source.today, date == selected, date in busy, (width - 4.dp) / 7)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DayCell(date: LocalDate, inMonth: Boolean, isToday: Boolean, isSelected: Boolean, hasEvents: Boolean, width: Dp) {
|
||||
val ink = when {
|
||||
isSelected -> GlanceTheme.colors.onPrimary
|
||||
isToday -> GlanceTheme.colors.primary
|
||||
inMonth -> GlanceTheme.colors.onSurface
|
||||
else -> GlanceTheme.colors.outline
|
||||
}
|
||||
// Always set: the host reapplies onto reused views, so a dropped background would linger.
|
||||
val fill = if (isSelected) GlanceTheme.colors.primary else ColorProvider(Color.Transparent)
|
||||
Column(
|
||||
modifier = GlanceModifier.cornerRadius(10.dp).background(fill)
|
||||
.width(width)
|
||||
.fillMaxHeight()
|
||||
.clickable(actionRunCallback<SelectDayAction>(actionParametersOf(SelectDayAction.dateKey to date.toString()))),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = date.day.toString(),
|
||||
maxLines = 1,
|
||||
style = TextStyle(
|
||||
color = ink,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = if (isToday || isSelected) FontWeight.Bold else FontWeight.Normal,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
)
|
||||
Text(
|
||||
text = if (hasEvents) "•" else " ",
|
||||
maxLines = 1,
|
||||
style = TextStyle(color = if (isSelected) ink else GlanceTheme.colors.primary, fontSize = 7.sp, textAlign = TextAlign.Center),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DayList(day: LocalDate, data: MonthDayData.Ready, preview: Boolean) {
|
||||
val context = LocalContext.current
|
||||
val source = data.source
|
||||
val zone = systemZone()
|
||||
val events = groupAgendaDays(day, day, source.instances, zone).firstOrNull()?.events.orEmpty()
|
||||
.let { all -> if (data.options.pastDisplay == PastEventDisplay.HIDE) all.filterNot { it.hasEnded(data.now) } else all }
|
||||
val metrics = compactRowMetrics(data.options.textSize, context.resources.configuration.fontScale)
|
||||
val openDay = actionStartActivity(MainActivity.openDateIntent(context, day, CalendarView.Month))
|
||||
Column(modifier = GlanceModifier.fillMaxSize()) {
|
||||
val date = localizedDateFormatter(widgetLocale(), "EEEdMMM").format(day.toJavaLocalDate())
|
||||
val label = when (day) {
|
||||
source.today, source.today.plus(1, DateTimeUnit.DAY) -> "${relativeDayLabel(context, day, source.today)} · $date"
|
||||
else -> date
|
||||
}
|
||||
Text(
|
||||
text = label,
|
||||
maxLines = 1,
|
||||
style = TextStyle(color = GlanceTheme.colors.onSurface, fontSize = metrics.time, fontWeight = FontWeight.Medium),
|
||||
modifier = GlanceModifier
|
||||
.padding(bottom = 4.dp)
|
||||
.wallpaperBacking(radius = 8.dp)
|
||||
.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||
.clickable(openDay),
|
||||
)
|
||||
if (events.isEmpty()) {
|
||||
Box(
|
||||
modifier = GlanceModifier.fillMaxSize().cornerRadius(14.dp).background(widgetCardColor()).clickable(openDay),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = context.getString(R.string.widget_day_empty),
|
||||
style = TextStyle(color = GlanceTheme.colors.onSurfaceVariant, fontSize = metrics.time, textAlign = TextAlign.Center),
|
||||
modifier = GlanceModifier.padding(8.dp),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val row: @Composable (Int) -> Unit = { i ->
|
||||
val event = events[i]
|
||||
WidgetEventRow(
|
||||
event = event,
|
||||
secondary = widgetTimeLabel(context, event, day, data.is24Hour),
|
||||
soften = source.soften,
|
||||
metrics = metrics,
|
||||
position = positionOf(i, events.size),
|
||||
source = CalendarView.Month,
|
||||
dimmed = data.options.pastDisplay == PastEventDisplay.DIM && event.hasEnded(data.now),
|
||||
)
|
||||
}
|
||||
if (preview) {
|
||||
Column(modifier = GlanceModifier.fillMaxSize()) { repeat(minOf(events.size, 8)) { row(it) } }
|
||||
} else {
|
||||
LazyColumn(modifier = GlanceModifier.fillMaxSize()) { items(events.size) { row(it) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package de.jeanlucmakiola.calendula.widget.today
|
||||
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetKind
|
||||
import de.jeanlucmakiola.calendula.widget.previewSizeOf
|
||||
import de.jeanlucmakiola.calendula.widget.provideSamplePreview
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.glance.GlanceId
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.GlanceTheme
|
||||
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.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.currentState
|
||||
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.fillMaxHeight
|
||||
import androidx.glance.layout.fillMaxSize
|
||||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.height
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.layout.width
|
||||
import androidx.glance.semantics.contentDescription
|
||||
import androidx.glance.semantics.semantics
|
||||
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||
import androidx.glance.text.FontWeight
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextAlign
|
||||
import androidx.glance.text.TextStyle
|
||||
import de.jeanlucmakiola.calendula.MainActivity
|
||||
import de.jeanlucmakiola.calendula.R
|
||||
import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay
|
||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||
import de.jeanlucmakiola.calendula.domain.hasEnded
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||
import de.jeanlucmakiola.calendula.widget.CalendulaWidgetReceiver
|
||||
import de.jeanlucmakiola.calendula.widget.CalendulaWidgetRoot
|
||||
import de.jeanlucmakiola.calendula.widget.DaysWidgetData
|
||||
import de.jeanlucmakiola.calendula.widget.EventRowMetrics
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetAppearance
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetEventRow
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetLoadContext
|
||||
import de.jeanlucmakiola.calendula.widget.compactRowMetrics
|
||||
import de.jeanlucmakiola.calendula.widget.loadDays
|
||||
import de.jeanlucmakiola.calendula.widget.relativeDayLabel
|
||||
import de.jeanlucmakiola.calendula.widget.widgetAppearance
|
||||
import de.jeanlucmakiola.calendula.widget.widgetLocale
|
||||
import de.jeanlucmakiola.calendula.widget.widgetLoadContext
|
||||
import de.jeanlucmakiola.calendula.widget.widgetTimeLabel
|
||||
import de.jeanlucmakiola.floret.components.positionOf
|
||||
import de.jeanlucmakiola.floret.glance.LocalWidgetBackground
|
||||
import de.jeanlucmakiola.floret.glance.WidgetBackground
|
||||
import de.jeanlucmakiola.floret.glance.WidgetData
|
||||
import de.jeanlucmakiola.floret.glance.WidgetMessage
|
||||
import de.jeanlucmakiola.floret.glance.currentRefreshStamp
|
||||
import de.jeanlucmakiola.floret.glance.refreshStamp
|
||||
import de.jeanlucmakiola.floret.glance.wallpaperBacking
|
||||
import de.jeanlucmakiola.floret.locale.localizedDateFormatter
|
||||
import de.jeanlucmakiola.floret.prefs.ThemeMode
|
||||
import kotlinx.datetime.DateTimeUnit
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.plus
|
||||
import kotlinx.datetime.toJavaDayOfWeek
|
||||
import kotlinx.datetime.toJavaLocalDate
|
||||
import java.time.format.TextStyle as JavaTextStyle
|
||||
|
||||
/**
|
||||
* Today list (#438): only today's events beside today's date pill. Once
|
||||
* they're over, or with none at all, it says so and shows tomorrow's first
|
||||
* one or two events in a quieter style.
|
||||
*/
|
||||
class TodayWidget : GlanceAppWidget() {
|
||||
|
||||
override val stateDefinition = PreferencesGlanceStateDefinition
|
||||
|
||||
// Exact: whether one or two of tomorrow's events fit follows the height.
|
||||
override val sizeMode = SizeMode.Exact
|
||||
|
||||
override val previewSizeMode = previewSizeOf(WidgetKind.TODAY)
|
||||
|
||||
override suspend fun providePreview(context: Context, widgetCategory: Int) =
|
||||
provideSamplePreview(context, WidgetKind.TODAY)
|
||||
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
val widgetData = WidgetData(context.loadTodayList(context.widgetLoadContext(id)), refreshStamp(context, id))
|
||||
provideContent {
|
||||
val loaded = widgetData.current(currentRefreshStamp()) { context.loadTodayList(context.widgetLoadContext(id)) }
|
||||
TodayWidgetContent(loaded.content, currentState<Preferences>().widgetAppearance, loaded.appTheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TodayWidgetReceiver : CalendulaWidgetReceiver() {
|
||||
override val glanceAppWidget: GlanceAppWidget = TodayWidget()
|
||||
}
|
||||
|
||||
internal suspend fun Context.loadTodayList(load: WidgetLoadContext) = loadDays(load) { today, _ ->
|
||||
today..today.plus(1, DateTimeUnit.DAY)
|
||||
}
|
||||
|
||||
private val PillWidth = 44.dp
|
||||
private val RowGap = 2.dp
|
||||
|
||||
/** What the list side shows: today's events, or the look ahead once none are left. */
|
||||
internal sealed interface TodayRows {
|
||||
data class Today(val events: List<EventInstance>) : TodayRows
|
||||
data class NothingLeft(val tomorrow: List<EventInstance>) : TodayRows
|
||||
}
|
||||
|
||||
internal fun todayRows(data: DaysWidgetData.Ready): TodayRows {
|
||||
val today = data.eventsOn(data.today)
|
||||
if (today.none { !it.hasEnded(data.now) }) {
|
||||
return TodayRows.NothingLeft(data.eventsOn(data.today.plus(1, DateTimeUnit.DAY)))
|
||||
}
|
||||
val shown = if (data.options.pastDisplay == PastEventDisplay.HIDE) today.filterNot { it.hasEnded(data.now) } else today
|
||||
return TodayRows.Today(shown)
|
||||
}
|
||||
|
||||
/** The whole widget, shared with its config preview, which draws its list as a plain column. */
|
||||
@Composable
|
||||
internal fun TodayWidgetContent(
|
||||
data: DaysWidgetData,
|
||||
appearance: WidgetAppearance,
|
||||
appTheme: ThemeMode,
|
||||
preview: Boolean = false,
|
||||
) {
|
||||
CalendulaWidgetRoot(appearance, appTheme) {
|
||||
val context = LocalContext.current
|
||||
when (data) {
|
||||
DaysWidgetData.NeedsPermission -> WidgetMessage(context.getString(R.string.widget_needs_permission))
|
||||
is DaysWidgetData.Ready -> Row(modifier = GlanceModifier.fillMaxSize().padding(6.dp)) {
|
||||
TodayPill(data.today)
|
||||
Spacer(GlanceModifier.width(6.dp))
|
||||
val metrics = compactRowMetrics(data.options.textSize, context.resources.configuration.fontScale)
|
||||
Box(modifier = GlanceModifier.defaultWeight().fillMaxHeight()) {
|
||||
when (val rows = todayRows(data)) {
|
||||
is TodayRows.Today -> TodayList(rows.events, data, metrics, preview)
|
||||
is TodayRows.NothingLeft -> NothingLeft(rows.tomorrow, data, metrics)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TodayList(events: List<EventInstance>, data: DaysWidgetData.Ready, metrics: EventRowMetrics, preview: Boolean) {
|
||||
val context = LocalContext.current
|
||||
val row: @Composable (Int) -> Unit = { index ->
|
||||
val event = events[index]
|
||||
WidgetEventRow(
|
||||
event = event,
|
||||
secondary = widgetTimeLabel(context, event, data.today, data.is24Hour),
|
||||
soften = data.soften,
|
||||
metrics = metrics,
|
||||
position = positionOf(index, events.size),
|
||||
source = CalendarView.Day,
|
||||
dimmed = data.options.pastDisplay == PastEventDisplay.DIM && event.hasEnded(data.now),
|
||||
)
|
||||
}
|
||||
if (preview) {
|
||||
val fits = ((LocalSize.current.height - 12.dp + RowGap) / (metrics.rowHeight + RowGap)).toInt().coerceAtLeast(1)
|
||||
Column(modifier = GlanceModifier.fillMaxSize()) { repeat(minOf(events.size, fits)) { row(it) } }
|
||||
} else {
|
||||
LazyColumn(modifier = GlanceModifier.fillMaxSize()) { items(events.size) { row(it) } }
|
||||
}
|
||||
}
|
||||
|
||||
/** "Nothing left today", then as many of tomorrow's first two events as the height holds. */
|
||||
@Composable
|
||||
private fun NothingLeft(tomorrow: List<EventInstance>, data: DaysWidgetData.Ready, metrics: EventRowMetrics) {
|
||||
val context = LocalContext.current
|
||||
val fontScale = context.resources.configuration.fontScale
|
||||
val lineHeight = 20.dp * fontScale
|
||||
val room = LocalSize.current.height - 12.dp - lineHeight
|
||||
val fits = ((room + RowGap) / (metrics.rowHeight + RowGap)).toInt().coerceIn(0, 2)
|
||||
val ahead = tomorrow.take(fits)
|
||||
val backing = if (LocalWidgetBackground.current == WidgetBackground.TRANSPARENT) {
|
||||
GlanceModifier.wallpaperBacking(radius = 8.dp).padding(horizontal = 8.dp)
|
||||
} else {
|
||||
GlanceModifier.padding(horizontal = 4.dp)
|
||||
}
|
||||
Column(modifier = GlanceModifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = context.getString(R.string.widget_nothing_left_today),
|
||||
maxLines = 1,
|
||||
style = TextStyle(color = GlanceTheme.colors.onSurfaceVariant, fontSize = metrics.title, fontWeight = FontWeight.Medium),
|
||||
modifier = backing.height(lineHeight),
|
||||
)
|
||||
val tomorrowDate = data.today.plus(1, DateTimeUnit.DAY)
|
||||
val tomorrowLabel = relativeDayLabel(context, tomorrowDate, data.today)
|
||||
ahead.forEachIndexed { index, event ->
|
||||
WidgetEventRow(
|
||||
event = event,
|
||||
secondary = "$tomorrowLabel · ${widgetTimeLabel(context, event, tomorrowDate, data.is24Hour)}",
|
||||
soften = data.soften,
|
||||
metrics = metrics,
|
||||
position = positionOf(index, ahead.size),
|
||||
source = CalendarView.Day,
|
||||
quiet = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Today's weekday and number, filled like the agenda's today pill; opens today. */
|
||||
@Composable
|
||||
private fun TodayPill(today: LocalDate) {
|
||||
val context = LocalContext.current
|
||||
val locale = widgetLocale()
|
||||
Column(
|
||||
modifier = GlanceModifier
|
||||
.width(PillWidth)
|
||||
.fillMaxHeight()
|
||||
.cornerRadius(PillWidth / 2)
|
||||
.background(GlanceTheme.colors.primary)
|
||||
.clickable(actionStartActivity(MainActivity.openDateIntent(context, today, CalendarView.Day)))
|
||||
.semantics { contentDescription = localizedDateFormatter(locale, "EEEEdMMMM").format(today.toJavaLocalDate()) },
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = today.dayOfWeek.toJavaDayOfWeek().getDisplayName(JavaTextStyle.SHORT, locale),
|
||||
maxLines = 1,
|
||||
style = TextStyle(color = GlanceTheme.colors.onPrimary, fontSize = 11.sp, textAlign = TextAlign.Center),
|
||||
)
|
||||
Text(
|
||||
text = today.day.toString(),
|
||||
maxLines = 1,
|
||||
style = TextStyle(
|
||||
color = GlanceTheme.colors.onPrimary,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package de.jeanlucmakiola.calendula.widget.upcoming
|
||||
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetKind
|
||||
import de.jeanlucmakiola.calendula.widget.previewSizeOf
|
||||
import de.jeanlucmakiola.calendula.widget.provideSamplePreview
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.glance.GlanceId
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.GlanceTheme
|
||||
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.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.width
|
||||
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||
import androidx.glance.text.FontWeight
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextStyle
|
||||
import de.jeanlucmakiola.calendula.MainActivity
|
||||
import de.jeanlucmakiola.calendula.R
|
||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||
import de.jeanlucmakiola.calendula.domain.hasEnded
|
||||
import de.jeanlucmakiola.calendula.ui.agenda.dayCount
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||
import de.jeanlucmakiola.calendula.widget.CalendulaWidgetReceiver
|
||||
import de.jeanlucmakiola.calendula.widget.CalendulaWidgetRoot
|
||||
import de.jeanlucmakiola.calendula.widget.DaysWidgetData
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetAppearance
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetEventRow
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetLoadContext
|
||||
import de.jeanlucmakiola.calendula.widget.compactRowMetrics
|
||||
import de.jeanlucmakiola.calendula.widget.loadDays
|
||||
import de.jeanlucmakiola.calendula.widget.relativeDayLabel
|
||||
import de.jeanlucmakiola.calendula.widget.widgetAppearance
|
||||
import de.jeanlucmakiola.calendula.widget.widgetLocale
|
||||
import de.jeanlucmakiola.calendula.widget.widgetLoadContext
|
||||
import de.jeanlucmakiola.calendula.widget.widgetTimeLabel
|
||||
import de.jeanlucmakiola.floret.components.positionOf
|
||||
import de.jeanlucmakiola.floret.glance.LocalWidgetBackground
|
||||
import de.jeanlucmakiola.floret.glance.WidgetBackground
|
||||
import de.jeanlucmakiola.floret.glance.WidgetData
|
||||
import de.jeanlucmakiola.floret.glance.WidgetMessage
|
||||
import de.jeanlucmakiola.floret.glance.currentRefreshStamp
|
||||
import de.jeanlucmakiola.floret.glance.refreshStamp
|
||||
import de.jeanlucmakiola.floret.glance.wallpaperBacking
|
||||
import de.jeanlucmakiola.floret.prefs.ThemeMode
|
||||
import kotlinx.datetime.DateTimeUnit
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.plus
|
||||
import kotlinx.datetime.toJavaDayOfWeek
|
||||
import java.time.format.TextStyle as JavaTextStyle
|
||||
|
||||
/**
|
||||
* 2×2 upcoming (#437): today's date on top, then the next events across the
|
||||
* widget's range in compact rows, as many as its height holds. The date opens
|
||||
* the agenda, an event opens itself.
|
||||
*/
|
||||
class UpcomingWidget : GlanceAppWidget() {
|
||||
|
||||
override val stateDefinition = PreferencesGlanceStateDefinition
|
||||
|
||||
// Exact: how many rows show follows the height.
|
||||
override val sizeMode = SizeMode.Exact
|
||||
|
||||
override val previewSizeMode = previewSizeOf(WidgetKind.UPCOMING)
|
||||
|
||||
override suspend fun providePreview(context: Context, widgetCategory: Int) =
|
||||
provideSamplePreview(context, WidgetKind.UPCOMING)
|
||||
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
val widgetData = WidgetData(context.loadUpcoming(context.widgetLoadContext(id)), refreshStamp(context, id))
|
||||
provideContent {
|
||||
val loaded = widgetData.current(currentRefreshStamp()) { context.loadUpcoming(context.widgetLoadContext(id)) }
|
||||
UpcomingWidgetContent(loaded.content, currentState<Preferences>().widgetAppearance, loaded.appTheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UpcomingWidgetReceiver : CalendulaWidgetReceiver() {
|
||||
override val glanceAppWidget: GlanceAppWidget = UpcomingWidget()
|
||||
}
|
||||
|
||||
internal suspend fun Context.loadUpcoming(load: WidgetLoadContext) = loadDays(load) { today, weekStart ->
|
||||
today..today.plus(load.options.range.dayCount(today, weekStart) - 1, DateTimeUnit.DAY)
|
||||
}
|
||||
|
||||
/** Most rows a tall widget lists; past this it is an agenda, which is its own widget. */
|
||||
private const val MAX_ROWS = 8
|
||||
|
||||
private val RowGap = 2.dp
|
||||
|
||||
/** Every event still to come or running, each once, on the first day it shows. */
|
||||
internal fun upcomingEvents(data: DaysWidgetData.Ready): List<Pair<LocalDate, EventInstance>> =
|
||||
data.days
|
||||
.flatMap { day -> day.events.map { day.date to it } }
|
||||
.filterNot { (_, event) -> event.hasEnded(data.now) }
|
||||
.distinctBy { (_, event) -> event.instanceId to event.start }
|
||||
|
||||
@Composable
|
||||
internal fun UpcomingWidgetContent(data: DaysWidgetData, appearance: WidgetAppearance, appTheme: ThemeMode) {
|
||||
CalendulaWidgetRoot(appearance, appTheme) {
|
||||
val context = LocalContext.current
|
||||
when (data) {
|
||||
DaysWidgetData.NeedsPermission -> WidgetMessage(context.getString(R.string.widget_needs_permission))
|
||||
is DaysWidgetData.Ready -> {
|
||||
val fontScale = context.resources.configuration.fontScale
|
||||
val metrics = compactRowMetrics(data.options.textSize, fontScale)
|
||||
val headerHeight = 44.dp * fontScale
|
||||
val room = LocalSize.current.height - 16.dp - headerHeight
|
||||
val fits = ((room + RowGap) / (metrics.rowHeight + RowGap)).toInt().coerceIn(1, MAX_ROWS)
|
||||
val events = upcomingEvents(data).take(fits)
|
||||
Column(modifier = GlanceModifier.fillMaxSize().padding(8.dp)) {
|
||||
DateHeader(data.today, headerHeight)
|
||||
if (events.isEmpty()) {
|
||||
WidgetMessage(context.getString(R.string.widget_upcoming_empty))
|
||||
} else {
|
||||
events.forEachIndexed { index, (day, event) ->
|
||||
val time = widgetTimeLabel(context, event, day, data.is24Hour)
|
||||
WidgetEventRow(
|
||||
event = event,
|
||||
secondary = if (day == data.today) time else "${relativeDayLabel(context, day, data.today)} · $time",
|
||||
soften = data.soften,
|
||||
metrics = metrics,
|
||||
position = positionOf(index, events.size),
|
||||
source = CalendarView.Agenda,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Today's number beside its weekday and month; opens the agenda. */
|
||||
@Composable
|
||||
private fun DateHeader(today: LocalDate, height: Dp) {
|
||||
val context = LocalContext.current
|
||||
val locale = widgetLocale()
|
||||
val backing = if (LocalWidgetBackground.current == WidgetBackground.TRANSPARENT) {
|
||||
GlanceModifier.wallpaperBacking(radius = 12.dp).padding(horizontal = 8.dp)
|
||||
} else {
|
||||
GlanceModifier.padding(horizontal = 4.dp)
|
||||
}
|
||||
Box(modifier = GlanceModifier.fillMaxWidth().height(height), contentAlignment = Alignment.CenterStart) {
|
||||
Row(
|
||||
modifier = backing.clickable(actionStartActivity(MainActivity.openViewIntent(context, CalendarView.Agenda))),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = today.day.toString(),
|
||||
maxLines = 1,
|
||||
style = TextStyle(color = GlanceTheme.colors.primary, fontSize = 28.sp, fontWeight = FontWeight.Bold),
|
||||
)
|
||||
Spacer(GlanceModifier.width(8.dp))
|
||||
Column {
|
||||
Text(
|
||||
text = today.dayOfWeek.toJavaDayOfWeek().getDisplayName(JavaTextStyle.FULL, locale),
|
||||
maxLines = 1,
|
||||
style = TextStyle(color = GlanceTheme.colors.onSurface, fontSize = 13.sp, fontWeight = FontWeight.Medium),
|
||||
)
|
||||
Text(
|
||||
text = java.time.Month.of(today.month.ordinal + 1).getDisplayName(JavaTextStyle.FULL_STANDALONE, locale),
|
||||
maxLines = 1,
|
||||
style = TextStyle(color = GlanceTheme.colors.onSurfaceVariant, fontSize = 12.sp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package de.jeanlucmakiola.calendula.widget.week
|
||||
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetKind
|
||||
import de.jeanlucmakiola.calendula.widget.previewSizeOf
|
||||
import de.jeanlucmakiola.calendula.widget.provideSamplePreview
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.glance.ColorFilter
|
||||
import androidx.glance.GlanceId
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.GlanceTheme
|
||||
import androidx.glance.Image
|
||||
import androidx.glance.ImageProvider
|
||||
import androidx.glance.LocalContext
|
||||
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.cornerRadius
|
||||
import androidx.glance.appwidget.provideContent
|
||||
import androidx.glance.background
|
||||
import androidx.glance.currentState
|
||||
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.fillMaxHeight
|
||||
import androidx.glance.layout.fillMaxSize
|
||||
import androidx.glance.layout.height
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.layout.size
|
||||
import androidx.glance.layout.width
|
||||
import androidx.glance.semantics.contentDescription
|
||||
import androidx.glance.semantics.semantics
|
||||
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||
import androidx.glance.text.FontWeight
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextAlign
|
||||
import androidx.glance.text.TextStyle
|
||||
import androidx.glance.unit.ColorProvider
|
||||
import de.jeanlucmakiola.calendula.MainActivity
|
||||
import de.jeanlucmakiola.calendula.R
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||
import de.jeanlucmakiola.calendula.ui.common.eventAccent
|
||||
import de.jeanlucmakiola.calendula.ui.common.eventFill
|
||||
import de.jeanlucmakiola.calendula.widget.CalendulaWidgetReceiver
|
||||
import de.jeanlucmakiola.calendula.widget.CalendulaWidgetRoot
|
||||
import de.jeanlucmakiola.calendula.widget.DaysWidgetData
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetAppearance
|
||||
import de.jeanlucmakiola.calendula.widget.loadDays
|
||||
import de.jeanlucmakiola.calendula.widget.weekOf
|
||||
import de.jeanlucmakiola.calendula.widget.widgetAppearance
|
||||
import de.jeanlucmakiola.calendula.widget.widgetLocale
|
||||
import de.jeanlucmakiola.calendula.widget.widgetCardColor
|
||||
import de.jeanlucmakiola.calendula.widget.widgetLoadContext
|
||||
import de.jeanlucmakiola.floret.glance.WidgetData
|
||||
import de.jeanlucmakiola.floret.glance.WidgetMessage
|
||||
import de.jeanlucmakiola.floret.glance.currentRefreshStamp
|
||||
import de.jeanlucmakiola.floret.glance.refreshStamp
|
||||
import de.jeanlucmakiola.floret.glance.themedColor
|
||||
import de.jeanlucmakiola.floret.locale.localizedDateFormatter
|
||||
import de.jeanlucmakiola.floret.prefs.ThemeMode
|
||||
import kotlinx.datetime.DateTimeUnit
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.plus
|
||||
import kotlinx.datetime.toJavaDayOfWeek
|
||||
import kotlinx.datetime.toJavaLocalDate
|
||||
import java.time.format.TextStyle as JavaTextStyle
|
||||
|
||||
/**
|
||||
* 4×1 week strip (#436): the current week's seven days from the app's week
|
||||
* start, today filled, a dot per event colour under each day. A day opens
|
||||
* its day view.
|
||||
*/
|
||||
class WeekStripWidget : GlanceAppWidget() {
|
||||
|
||||
override val stateDefinition = PreferencesGlanceStateDefinition
|
||||
|
||||
override val sizeMode = SizeMode.Single
|
||||
|
||||
override val previewSizeMode = previewSizeOf(WidgetKind.WEEK_STRIP)
|
||||
|
||||
override suspend fun providePreview(context: Context, widgetCategory: Int) =
|
||||
provideSamplePreview(context, WidgetKind.WEEK_STRIP)
|
||||
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
val widgetData = WidgetData(context.loadWeek(id), refreshStamp(context, id))
|
||||
provideContent {
|
||||
val loaded = widgetData.current(currentRefreshStamp()) { context.loadWeek(id) }
|
||||
WeekStripWidgetContent(loaded.content, currentState<Preferences>().widgetAppearance, loaded.appTheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WeekStripWidgetReceiver : CalendulaWidgetReceiver() {
|
||||
override val glanceAppWidget: GlanceAppWidget = WeekStripWidget()
|
||||
}
|
||||
|
||||
private suspend fun Context.loadWeek(id: GlanceId) = loadDays(widgetLoadContext(id), ::weekOf)
|
||||
|
||||
/** Most dots a day shows; more colours than this read as noise at this size. */
|
||||
private const val MAX_DOTS = 3
|
||||
|
||||
private val DayGap = 4.dp
|
||||
private val DotSize = 5.dp
|
||||
|
||||
@Composable
|
||||
internal fun WeekStripWidgetContent(data: DaysWidgetData, appearance: WidgetAppearance, appTheme: ThemeMode) {
|
||||
CalendulaWidgetRoot(appearance, appTheme) {
|
||||
when (data) {
|
||||
DaysWidgetData.NeedsPermission ->
|
||||
WidgetMessage(LocalContext.current.getString(R.string.widget_needs_permission))
|
||||
is DaysWidgetData.Ready -> Row(
|
||||
modifier = GlanceModifier.fillMaxSize().padding(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
for (i in 0 until 7) {
|
||||
if (i > 0) Spacer(GlanceModifier.width(DayGap))
|
||||
Box(modifier = GlanceModifier.defaultWeight().fillMaxHeight()) {
|
||||
DayTile(data.firstDay.plus(i, DateTimeUnit.DAY), data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DayTile(date: LocalDate, data: DaysWidgetData.Ready) {
|
||||
val context = LocalContext.current
|
||||
val isToday = date == data.today
|
||||
val container = if (isToday) GlanceTheme.colors.primary else widgetCardColor()
|
||||
val ink = when {
|
||||
isToday -> GlanceTheme.colors.onPrimary
|
||||
date < data.today -> GlanceTheme.colors.onSurfaceVariant
|
||||
else -> GlanceTheme.colors.onSurface
|
||||
}
|
||||
val locale = widgetLocale()
|
||||
val colors = data.eventsOn(date).map { it.color }.distinct()
|
||||
val label = localizedDateFormatter(locale, "EEEEdMMMM").format(date.toJavaLocalDate())
|
||||
Column(
|
||||
modifier = GlanceModifier
|
||||
.fillMaxSize()
|
||||
.cornerRadius(14.dp)
|
||||
.background(container)
|
||||
.clickable(actionStartActivity(MainActivity.openDateIntent(context, date, CalendarView.Week)))
|
||||
.semantics {
|
||||
contentDescription = if (colors.isEmpty()) label else context.resources.getQuantityString(
|
||||
R.plurals.widget_day_with_events, data.eventsOn(date).size, label, data.eventsOn(date).size,
|
||||
)
|
||||
},
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = date.dayOfWeek.toJavaDayOfWeek().getDisplayName(JavaTextStyle.SHORT, locale),
|
||||
maxLines = 1,
|
||||
style = TextStyle(color = ink, fontSize = 11.sp, textAlign = TextAlign.Center),
|
||||
)
|
||||
Text(
|
||||
text = date.day.toString(),
|
||||
maxLines = 1,
|
||||
style = TextStyle(color = ink, fontSize = 17.sp, fontWeight = FontWeight.Medium, textAlign = TextAlign.Center),
|
||||
)
|
||||
Spacer(GlanceModifier.height(3.dp))
|
||||
// A dot row is always drawn, so the numbers line up across days.
|
||||
Row(modifier = GlanceModifier.height(DotSize)) {
|
||||
colors.take(MAX_DOTS).forEachIndexed { index, color ->
|
||||
if (index > 0) Spacer(GlanceModifier.width(3.dp))
|
||||
Dot(dotColor(color, isToday, data.soften))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A mark on the tile: the accent tone on a card, the lighter fill tone on today's primary. */
|
||||
@Composable
|
||||
private fun dotColor(color: Int, onPrimary: Boolean, soften: Boolean): ColorProvider = if (onPrimary) {
|
||||
themedColor(day = eventFill(color, dark = false, soften), night = eventFill(color, dark = true, soften))
|
||||
} else {
|
||||
themedColor(day = eventAccent(color, dark = false, soften), night = eventAccent(color, dark = true, soften))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Dot(color: ColorProvider) {
|
||||
Image(
|
||||
provider = ImageProvider(R.drawable.widget_dot),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(color),
|
||||
modifier = GlanceModifier.size(DotSize),
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#4F7CAC" />
|
||||
<solid android:color="#FFFFFF" />
|
||||
<corners android:radius="3dp" />
|
||||
</shape>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/widget_preview_cell" />
|
||||
<corners android:radius="12dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#FFFFFF" />
|
||||
<corners android:radius="14dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/widget_preview_outside" />
|
||||
<corners android:radius="12dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/widget_preview_primary" />
|
||||
<corners android:radius="14dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="#FFFFFFFF" />
|
||||
</shape>
|
||||
@@ -1,124 +1,186 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Static mock shown in the widget picker (android:previewLayout). Not the
|
||||
runtime layout — the live widget is rendered by Glance. Colours are the
|
||||
brand light scheme so the picker preview reads on its light sheet. -->
|
||||
<!-- Static mock shown in the widget picker on Android 14 and below
|
||||
(android:previewLayout); 15+ shows the generated preview instead. -->
|
||||
<LinearLayout 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:orientation="vertical"
|
||||
android:background="@drawable/preview_widget_bg"
|
||||
android:padding="14dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/widget_agenda_title"
|
||||
android:textColor="@color/widget_preview_primary"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/agenda_header_today"
|
||||
android:textColor="@color/widget_preview_primary"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp"
|
||||
tools:ignore="HardcodedText,NestedWeights,DisableBaselineAlignment,SmallSp,TooManyViews,UselessParent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
<ImageView
|
||||
android:layout_width="5dp"
|
||||
android:layout_height="34dp"
|
||||
android:contentDescription="@null"
|
||||
android:background="@drawable/preview_stripe"
|
||||
android:backgroundTint="#4F7CAC" />
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:maxLines="1"
|
||||
android:paddingHorizontal="4dp"
|
||||
android:text="@string/widget_agenda_title"
|
||||
android:textColor="@color/widget_preview_primary"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold" />
|
||||
<TextView style="@style/WidgetPreviewAddButton" android:text="+" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/preview_widget_today_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:paddingTop="6dp"
|
||||
android:text="Mon 26"
|
||||
android:textColor="@color/widget_preview_on_primary"
|
||||
android:textSize="13sp"
|
||||
tools:ignore="HardcodedText" />
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:orientation="vertical">
|
||||
android:layout_marginTop="0dp"
|
||||
android:background="@drawable/preview_widget_event_bg"
|
||||
android:backgroundTint="@color/widget_preview_event_blue"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:paddingVertical="3dp">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Standup"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="14sp" />
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@string/settings_past_events_sample_morning"
|
||||
android:textColor="@color/widget_preview_on_event_light"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="09:00 – 09:30"
|
||||
android:textColor="@color/widget_preview_variant"
|
||||
android:textSize="12sp" />
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="9:30 – 10:00"
|
||||
android:textColor="@color/widget_preview_on_event_light"
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="3dp"
|
||||
android:background="@drawable/preview_widget_event_bg"
|
||||
android:backgroundTint="@color/widget_preview_event_green"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:paddingVertical="3dp">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@string/settings_past_events_sample_midday"
|
||||
android:textColor="@color/widget_preview_on_event_light"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="12:30 – 13:30"
|
||||
android:textColor="@color/widget_preview_on_event_light"
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="3dp"
|
||||
android:background="@drawable/preview_widget_event_bg"
|
||||
android:backgroundTint="@color/widget_preview_event_purple"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:paddingVertical="3dp">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@string/settings_past_events_sample_evening"
|
||||
android:textColor="@color/widget_preview_on_event_dark"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="19:00 – 21:00"
|
||||
android:textColor="@color/widget_preview_on_event_dark"
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
<ImageView
|
||||
android:layout_width="5dp"
|
||||
android:layout_height="34dp"
|
||||
android:contentDescription="@null"
|
||||
android:background="@drawable/preview_stripe"
|
||||
android:backgroundTint="#6FA37A" />
|
||||
android:layout_marginTop="6dp"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/preview_widget_today_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:paddingTop="6dp"
|
||||
android:text="Tue 27"
|
||||
android:textColor="@color/widget_preview_on_primary"
|
||||
android:textSize="13sp"
|
||||
tools:ignore="HardcodedText" />
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:orientation="vertical">
|
||||
android:layout_marginTop="0dp"
|
||||
android:background="@drawable/preview_widget_event_bg"
|
||||
android:backgroundTint="@color/widget_preview_event_orange"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:paddingVertical="3dp">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Design review"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="14sp" />
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@string/widget_preview_sample_trip"
|
||||
android:textColor="@color/widget_preview_on_event_light"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="11:30 – 12:30"
|
||||
android:textColor="@color/widget_preview_variant"
|
||||
android:textSize="12sp" />
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@string/event_detail_all_day"
|
||||
android:textColor="@color/widget_preview_on_event_light"
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
<ImageView
|
||||
android:layout_width="5dp"
|
||||
android:layout_height="34dp"
|
||||
android:contentDescription="@null"
|
||||
android:background="@drawable/preview_stripe"
|
||||
android:backgroundTint="#C58A56" />
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="1:1 with Mara"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="14sp" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="14:00 – 14:30"
|
||||
android:textColor="@color/widget_preview_variant"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Static mock shown in the widget picker on Android 14 and below
|
||||
(android:previewLayout); 15+ shows the generated preview instead. -->
|
||||
<LinearLayout 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:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:padding="12dp"
|
||||
tools:ignore="HardcodedText,NestedWeights,DisableBaselineAlignment,SmallSp,TooManyViews,UselessParent">
|
||||
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/preview_stripe"
|
||||
android:backgroundTint="@color/widget_preview_event_blue" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1"
|
||||
android:text="@string/settings_past_events_sample_morning"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="13sp" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1"
|
||||
android:text="in 2 h"
|
||||
android:textColor="@color/widget_preview_primary"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1"
|
||||
android:text="9:30"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Static mock shown in the widget picker on Android 14 and below
|
||||
(android:previewLayout); 15+ shows the generated preview instead. -->
|
||||
<LinearLayout 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:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="4dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="MON"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="11sp"
|
||||
android:textStyle="bold"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/floret_widget_shape_cookie"
|
||||
android:tint="@color/widget_preview_primary"
|
||||
tools:ignore="UseAppTint" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="26"
|
||||
android:textColor="@color/widget_preview_on_primary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
tools:ignore="HardcodedText" />
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
@@ -1,114 +1,776 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Static mock shown in the widget picker (android:previewLayout). The live
|
||||
widget is rendered by Glance with real events; this only needs to read as
|
||||
"a month grid" in the picker. -->
|
||||
<!-- Static mock shown in the widget picker on Android 14 and below
|
||||
(android:previewLayout); 15+ shows the generated preview instead. -->
|
||||
<LinearLayout 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:orientation="vertical"
|
||||
android:background="@drawable/preview_widget_bg"
|
||||
android:padding="12dp">
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp"
|
||||
tools:ignore="HardcodedText,NestedWeights,DisableBaselineAlignment,SmallSp,TooManyViews,UselessParent">
|
||||
|
||||
<!-- Header -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="‹"
|
||||
android:textColor="@color/widget_preview_variant"
|
||||
android:textSize="16sp" />
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="June 2026"
|
||||
android:maxLines="1"
|
||||
android:paddingHorizontal="4dp"
|
||||
android:text="October"
|
||||
android:textColor="@color/widget_preview_primary"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold" />
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="›"
|
||||
android:textColor="@color/widget_preview_variant"
|
||||
android:textSize="16sp" />
|
||||
<TextView style="@style/WidgetPreviewHeaderButton" android:text="‹" />
|
||||
<TextView style="@style/WidgetPreviewHeaderButton" android:layout_width="28dp" android:text="26" android:textStyle="bold" />
|
||||
<TextView style="@style/WidgetPreviewHeaderButton" android:text="›" />
|
||||
<TextView style="@style/WidgetPreviewAddButton" android:text="+" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Weekday header -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginBottom="1dp"
|
||||
android:orientation="horizontal">
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="M" android:textColor="@color/widget_preview_variant" android:textSize="11sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="T" android:textColor="@color/widget_preview_variant" android:textSize="11sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="W" android:textColor="@color/widget_preview_variant" android:textSize="11sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="T" android:textColor="@color/widget_preview_variant" android:textSize="11sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="F" android:textColor="@color/widget_preview_variant" android:textSize="11sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="S" android:textColor="@color/widget_preview_variant" android:textSize="11sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="S" android:textColor="@color/widget_preview_variant" android:textSize="11sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center"
|
||||
android:text="M"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center"
|
||||
android:text="T"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center"
|
||||
android:text="W"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center"
|
||||
android:text="T"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center"
|
||||
android:text="F"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center"
|
||||
android:text="S"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center"
|
||||
android:text="S"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Week 1: 1..7 (event bar on the 3rd) -->
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="horizontal" android:layout_marginTop="4dp">
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="1" android:textColor="@color/widget_preview_on_surface" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="2" android:textColor="@color/widget_preview_on_surface" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="3" android:textColor="#4F7CAC" android:textStyle="bold" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="4" android:textColor="@color/widget_preview_on_surface" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="5" android:textColor="@color/widget_preview_on_surface" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="6" android:textColor="@color/widget_preview_on_surface" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="7" android:textColor="@color/widget_preview_on_surface" android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Week 2: 8..14 (bars on 11,12,13) -->
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="horizontal" android:layout_marginTop="4dp">
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="8" android:textColor="@color/widget_preview_on_surface" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="9" android:textColor="@color/widget_preview_on_surface" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="10" android:textColor="@color/widget_preview_on_surface" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="11" android:textColor="#6FA37A" android:textStyle="bold" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="12" android:textColor="#6FA37A" android:textStyle="bold" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="13" android:textColor="#6FA37A" android:textStyle="bold" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="14" android:textColor="@color/widget_preview_on_surface" android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Week 3: 15..21 (17 = today) -->
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="horizontal" android:layout_marginTop="4dp">
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="15" android:textColor="@color/widget_preview_on_surface" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="16" android:textColor="@color/widget_preview_on_surface" android:textSize="12sp" />
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:orientation="horizontal">
|
||||
<TextView android:layout_width="22dp" android:layout_height="22dp" android:gravity="center"
|
||||
android:text="17" android:textColor="@color/widget_preview_on_primary" android:textStyle="bold" android:textSize="12sp"
|
||||
android:background="@drawable/preview_today_circle" />
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_outside_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="28"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_outside_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="29"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_outside_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="30"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="1"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="2"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="3"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="4"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="18" android:textColor="@color/widget_preview_on_surface" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="19" android:textColor="@color/widget_preview_on_surface" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="20" android:textColor="@color/widget_preview_on_surface" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="21" android:textColor="@color/widget_preview_on_surface" android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Week 4: 22..28 (bar on 24) -->
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="horizontal" android:layout_marginTop="4dp">
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="22" android:textColor="@color/widget_preview_on_surface" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="23" android:textColor="@color/widget_preview_on_surface" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="24" android:textColor="#C58A56" android:textStyle="bold" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="25" android:textColor="@color/widget_preview_on_surface" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="26" android:textColor="@color/widget_preview_on_surface" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="27" android:textColor="@color/widget_preview_on_surface" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="28" android:textColor="@color/widget_preview_on_surface" android:textSize="12sp" />
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="5"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="6"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="7"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="8"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="9"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="10"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="11"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Week 5: 29, 30, then trailing July days greyed -->
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="horizontal" android:layout_marginTop="4dp">
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="29" android:textColor="@color/widget_preview_on_surface" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="30" android:textColor="@color/widget_preview_on_surface" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="1" android:textColor="@color/widget_preview_variant" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="2" android:textColor="@color/widget_preview_variant" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="3" android:textColor="@color/widget_preview_variant" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="4" android:textColor="@color/widget_preview_variant" android:textSize="12sp" />
|
||||
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="5" android:textColor="@color/widget_preview_variant" android:textSize="12sp" />
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="12"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="13"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="14"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="15"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="16"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="17"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="18"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="19"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="20"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="2dp"
|
||||
android:background="@drawable/preview_stripe"
|
||||
android:backgroundTint="@color/widget_preview_event_blue"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:paddingHorizontal="2dp"
|
||||
android:text="@string/settings_past_events_sample_morning"
|
||||
android:textColor="@color/widget_preview_on_event_light"
|
||||
android:textSize="8sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="21"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="22"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="23"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="24"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="2dp"
|
||||
android:background="@drawable/preview_stripe"
|
||||
android:backgroundTint="@color/widget_preview_event_purple"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:paddingHorizontal="2dp"
|
||||
android:text="@string/settings_past_events_sample_evening"
|
||||
android:textColor="@color/widget_preview_on_event_dark"
|
||||
android:textSize="8sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="25"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:background="@drawable/preview_today_circle"
|
||||
android:gravity="center"
|
||||
android:text="26"
|
||||
android:textColor="@color/widget_preview_on_primary"
|
||||
android:textSize="10sp"
|
||||
android:textStyle="bold" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="2dp"
|
||||
android:background="@drawable/preview_stripe"
|
||||
android:backgroundTint="@color/widget_preview_event_blue"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:paddingHorizontal="2dp"
|
||||
android:text="@string/settings_past_events_sample_morning"
|
||||
android:textColor="@color/widget_preview_on_event_light"
|
||||
android:textSize="8sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="27"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="2dp"
|
||||
android:background="@drawable/preview_stripe"
|
||||
android:backgroundTint="@color/widget_preview_event_orange"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:paddingHorizontal="2dp"
|
||||
android:text="@string/widget_preview_sample_trip"
|
||||
android:textColor="@color/widget_preview_on_event_light"
|
||||
android:textSize="8sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="28"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="2dp"
|
||||
android:background="@drawable/preview_stripe"
|
||||
android:backgroundTint="@color/widget_preview_event_red"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:paddingHorizontal="2dp"
|
||||
android:text="@string/widget_preview_sample_dentist"
|
||||
android:textColor="@color/widget_preview_on_event_dark"
|
||||
android:textSize="8sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="29"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="30"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_cell_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="31"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="2dp"
|
||||
android:background="@drawable/preview_stripe"
|
||||
android:backgroundTint="@color/widget_preview_event_purple"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:paddingHorizontal="2dp"
|
||||
android:text="@string/settings_past_events_sample_evening"
|
||||
android:textColor="@color/widget_preview_on_event_dark"
|
||||
android:textSize="8sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_outside_bg"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center"
|
||||
android:text="1"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -0,0 +1,518 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Static mock shown in the widget picker on Android 14 and below
|
||||
(android:previewLayout); 15+ shows the generated preview instead. -->
|
||||
<LinearLayout 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:orientation="vertical"
|
||||
android:padding="8dp"
|
||||
tools:ignore="HardcodedText,NestedWeights,DisableBaselineAlignment,SmallSp,TooManyViews,UselessParent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:maxLines="1"
|
||||
android:paddingHorizontal="4dp"
|
||||
android:text="October"
|
||||
android:textColor="@color/widget_preview_primary"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold" />
|
||||
<TextView style="@style/WidgetPreviewHeaderButton" android:text="‹" />
|
||||
<TextView style="@style/WidgetPreviewHeaderButton" android:layout_width="28dp" android:text="26" android:textStyle="bold" />
|
||||
<TextView style="@style/WidgetPreviewHeaderButton" android:text="›" />
|
||||
<TextView style="@style/WidgetPreviewAddButton" android:text="+" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1.1"
|
||||
android:orientation="vertical">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="M"
|
||||
android:textColor="@color/widget_preview_variant"
|
||||
android:textSize="9sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="T"
|
||||
android:textColor="@color/widget_preview_variant"
|
||||
android:textSize="9sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="W"
|
||||
android:textColor="@color/widget_preview_variant"
|
||||
android:textSize="9sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="T"
|
||||
android:textColor="@color/widget_preview_variant"
|
||||
android:textSize="9sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="F"
|
||||
android:textColor="@color/widget_preview_variant"
|
||||
android:textSize="9sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="S"
|
||||
android:textColor="@color/widget_preview_variant"
|
||||
android:textSize="9sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="S"
|
||||
android:textColor="@color/widget_preview_variant"
|
||||
android:textSize="9sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="28"
|
||||
android:textColor="@color/widget_preview_variant"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="29"
|
||||
android:textColor="@color/widget_preview_variant"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="30"
|
||||
android:textColor="@color/widget_preview_variant"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="1"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="2"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="3"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="4"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="5"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="6"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="7"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="8"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="9"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="10"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="11"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="12"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="13"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="14"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="15"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="16"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="17"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="18"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="19"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="20"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="21"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="22"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="23"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="24"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="25"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/preview_widget_today_bg"
|
||||
android:gravity="center"
|
||||
android:text="26"
|
||||
android:textColor="@color/widget_preview_on_primary"
|
||||
android:textSize="10sp"
|
||||
android:textStyle="bold" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="27"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="28"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="29"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="30"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="31"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="10sp" />
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="1"
|
||||
android:textColor="@color/widget_preview_variant"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="4dp"
|
||||
android:text="@string/widget_today"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="11sp"
|
||||
android:textStyle="bold" />
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="3dp"
|
||||
android:background="@drawable/preview_widget_event_bg"
|
||||
android:backgroundTint="@color/widget_preview_event_blue"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:paddingVertical="3dp">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@string/settings_past_events_sample_morning"
|
||||
android:textColor="@color/widget_preview_on_event_light"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="9:30 – 10:00"
|
||||
android:textColor="@color/widget_preview_on_event_light"
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="3dp"
|
||||
android:background="@drawable/preview_widget_event_bg"
|
||||
android:backgroundTint="@color/widget_preview_event_green"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:paddingVertical="3dp">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@string/settings_past_events_sample_midday"
|
||||
android:textColor="@color/widget_preview_on_event_light"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="12:30 – 13:30"
|
||||
android:textColor="@color/widget_preview_on_event_light"
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="3dp"
|
||||
android:background="@drawable/preview_widget_event_bg"
|
||||
android:backgroundTint="@color/widget_preview_event_purple"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:paddingVertical="3dp">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@string/settings_past_events_sample_evening"
|
||||
android:textColor="@color/widget_preview_on_event_dark"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="19:00 – 21:00"
|
||||
android:textColor="@color/widget_preview_on_event_dark"
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Static mock shown in the widget picker on Android 14 and below
|
||||
(android:previewLayout); 15+ shows the generated preview instead. -->
|
||||
<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>
|
||||
@@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Static mock shown in the widget picker on Android 14 and below
|
||||
(android:previewLayout); 15+ shows the generated preview instead. -->
|
||||
<LinearLayout 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:orientation="horizontal"
|
||||
android:padding="6dp"
|
||||
tools:ignore="HardcodedText,NestedWeights,DisableBaselineAlignment,SmallSp,TooManyViews,UselessParent">
|
||||
|
||||
<TextView
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/preview_widget_today_bg"
|
||||
android:gravity="center"
|
||||
android:text="Mon 26"
|
||||
android:textColor="@color/widget_preview_on_primary"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="0dp"
|
||||
android:background="@drawable/preview_widget_event_bg"
|
||||
android:backgroundTint="@color/widget_preview_event_blue"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:paddingVertical="3dp">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@string/settings_past_events_sample_morning"
|
||||
android:textColor="@color/widget_preview_on_event_light"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="9:30 – 10:00"
|
||||
android:textColor="@color/widget_preview_on_event_light"
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="3dp"
|
||||
android:background="@drawable/preview_widget_event_bg"
|
||||
android:backgroundTint="@color/widget_preview_event_green"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:paddingVertical="3dp">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@string/settings_past_events_sample_midday"
|
||||
android:textColor="@color/widget_preview_on_event_light"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="12:30 – 13:30"
|
||||
android:textColor="@color/widget_preview_on_event_light"
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Static mock shown in the widget picker on Android 14 and below
|
||||
(android:previewLayout); 15+ shows the generated preview instead. -->
|
||||
<LinearLayout 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:orientation="vertical"
|
||||
android:padding="8dp"
|
||||
tools:ignore="HardcodedText,NestedWeights,DisableBaselineAlignment,SmallSp,TooManyViews,UselessParent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="4dp">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="26"
|
||||
android:textColor="@color/widget_preview_primary"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold" />
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="Monday October"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:background="@drawable/preview_widget_event_bg"
|
||||
android:backgroundTint="@color/widget_preview_event_blue"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:paddingVertical="3dp">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@string/settings_past_events_sample_morning"
|
||||
android:textColor="@color/widget_preview_on_event_light"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="9:30 – 10:00"
|
||||
android:textColor="@color/widget_preview_on_event_light"
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="3dp"
|
||||
android:background="@drawable/preview_widget_event_bg"
|
||||
android:backgroundTint="@color/widget_preview_event_green"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:paddingVertical="3dp">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@string/settings_past_events_sample_midday"
|
||||
android:textColor="@color/widget_preview_on_event_light"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="12:30 – 13:30"
|
||||
android:textColor="@color/widget_preview_on_event_light"
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,202 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Static mock shown in the widget picker on Android 14 and below
|
||||
(android:previewLayout); 15+ shows the generated preview instead. -->
|
||||
<LinearLayout 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:orientation="horizontal"
|
||||
android:padding="6dp"
|
||||
tools:ignore="HardcodedText,NestedWeights,DisableBaselineAlignment,SmallSp,TooManyViews,UselessParent">
|
||||
|
||||
<LinearLayout
|
||||
style="@style/WidgetPreviewWeekDay"
|
||||
android:background="@drawable/preview_widget_today_bg"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Mon"
|
||||
android:textColor="@color/widget_preview_on_primary"
|
||||
android:textSize="11sp" />
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="26"
|
||||
android:textColor="@color/widget_preview_on_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="6dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginHorizontal="1dp"
|
||||
android:background="@drawable/preview_today_circle"
|
||||
android:backgroundTint="@color/widget_preview_event_blue" />
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginHorizontal="1dp"
|
||||
android:background="@drawable/preview_today_circle"
|
||||
android:backgroundTint="@color/widget_preview_event_green" />
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginHorizontal="1dp"
|
||||
android:background="@drawable/preview_today_circle"
|
||||
android:backgroundTint="@color/widget_preview_event_purple" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
style="@style/WidgetPreviewWeekDay"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Tue"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="11sp" />
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="27"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="6dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginHorizontal="1dp"
|
||||
android:background="@drawable/preview_today_circle"
|
||||
android:backgroundTint="@color/widget_preview_event_orange" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
style="@style/WidgetPreviewWeekDay"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Wed"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="11sp" />
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="28"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="6dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginHorizontal="1dp"
|
||||
android:background="@drawable/preview_today_circle"
|
||||
android:backgroundTint="@color/widget_preview_event_red" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
style="@style/WidgetPreviewWeekDay"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Thu"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="11sp" />
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="29"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
<Space
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="6dp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
style="@style/WidgetPreviewWeekDay"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Fri"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="11sp" />
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="30"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
<Space
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="6dp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
style="@style/WidgetPreviewWeekDay"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Sat"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="11sp" />
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="31"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="6dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginHorizontal="1dp"
|
||||
android:background="@drawable/preview_today_circle"
|
||||
android:backgroundTint="@color/widget_preview_event_purple" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
style="@style/WidgetPreviewWeekDay"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Sun"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="11sp" />
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="1"
|
||||
android:textColor="@color/widget_preview_on_surface"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
<Space
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="6dp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -1,6 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="widget_preview_surface">@android:color/system_neutral1_900</color>
|
||||
<color name="widget_preview_surface">@android:color/system_accent2_700</color>
|
||||
<color name="widget_preview_cell">@android:color/system_accent2_900</color>
|
||||
<color name="widget_preview_outside">@android:color/system_neutral2_600</color>
|
||||
<color name="widget_preview_on_surface">@android:color/system_neutral1_50</color>
|
||||
<color name="widget_preview_variant">@android:color/system_neutral2_200</color>
|
||||
<color name="widget_preview_primary">@android:color/system_accent1_200</color>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="widget_preview_surface">#101316</color>
|
||||
<color name="widget_preview_surface">#2B3742</color>
|
||||
<color name="widget_preview_cell">#1C252D</color>
|
||||
<color name="widget_preview_outside">#3A4651</color>
|
||||
<color name="widget_preview_on_surface">#E1E3E6</color>
|
||||
<color name="widget_preview_variant">#A8ADB2</color>
|
||||
<color name="widget_preview_primary">#A3CBE2</color>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="widget_preview_surface">@android:color/system_neutral1_50</color>
|
||||
<color name="widget_preview_surface">@android:color/system_accent2_100</color>
|
||||
<color name="widget_preview_cell">@android:color/system_accent2_10</color>
|
||||
<color name="widget_preview_outside">@android:color/system_neutral2_300</color>
|
||||
<color name="widget_preview_on_surface">@android:color/system_neutral1_900</color>
|
||||
<color name="widget_preview_variant">@android:color/system_neutral2_700</color>
|
||||
<color name="widget_preview_primary">@android:color/system_accent1_600</color>
|
||||
|
||||
@@ -435,6 +435,65 @@
|
||||
<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_date_label">Calendula date</string>
|
||||
<string name="widget_week_strip_label">Calendula week strip</string>
|
||||
<!-- A day in the week strip widget, for screen readers: "Wednesday, 7 October, 2 events" -->
|
||||
<plurals name="widget_day_with_events">
|
||||
<item quantity="one">%1$s, %2$d event</item>
|
||||
<item quantity="other">%1$s, %2$d events</item>
|
||||
</plurals>
|
||||
<string name="widget_upcoming_label">Calendula upcoming</string>
|
||||
<string name="widget_upcoming_empty">No upcoming events</string>
|
||||
<string name="widget_today_label">Calendula today</string>
|
||||
<string name="widget_nothing_left_today">Nothing left today</string>
|
||||
<string name="widget_countdown_label">Calendula countdown</string>
|
||||
<string name="widget_countdown_now">Now</string>
|
||||
<string name="widget_countdown_ending_now">Ending now</string>
|
||||
<string name="widget_countdown_tomorrow">Tomorrow</string>
|
||||
<string name="widget_countdown_ends_tomorrow">Ends tomorrow</string>
|
||||
<!-- A running all-day event's last day: "Until Fri" -->
|
||||
<string name="widget_countdown_until">Until %1$s</string>
|
||||
<plurals name="widget_countdown_in_minutes">
|
||||
<item quantity="one">in %d min</item>
|
||||
<item quantity="other">in %d min</item>
|
||||
</plurals>
|
||||
<plurals name="widget_countdown_in_hours">
|
||||
<item quantity="one">in %d h</item>
|
||||
<item quantity="other">in %d h</item>
|
||||
</plurals>
|
||||
<plurals name="widget_countdown_in_days">
|
||||
<item quantity="one">in %d day</item>
|
||||
<item quantity="other">in %d days</item>
|
||||
</plurals>
|
||||
<plurals name="widget_countdown_ends_minutes">
|
||||
<item quantity="one">ends in %d min</item>
|
||||
<item quantity="other">ends in %d min</item>
|
||||
</plurals>
|
||||
<plurals name="widget_countdown_ends_hours">
|
||||
<item quantity="one">ends in %d h</item>
|
||||
<item quantity="other">ends in %d h</item>
|
||||
</plurals>
|
||||
<plurals name="widget_countdown_ends_days">
|
||||
<item quantity="one">ends in %d day</item>
|
||||
<item quantity="other">ends in %d days</item>
|
||||
</plurals>
|
||||
<string name="widget_countdown_choose">Tap to choose an event to count down to</string>
|
||||
<!-- A pinned countdown whose event has no occurrence left: "Vacation is over" -->
|
||||
<string name="widget_countdown_over">%1$s is over</string>
|
||||
<string name="widget_config_countdown">Count down to</string>
|
||||
<string name="widget_config_countdown_next">Next event</string>
|
||||
<string name="widget_config_countdown_pinned">Chosen event</string>
|
||||
<string name="widget_config_countdown_next_hint">The next timed event in this widget\'s calendars, or the one running now. All-day and declined events are skipped.</string>
|
||||
<string name="widget_config_countdown_event">Event</string>
|
||||
<string name="widget_config_countdown_event_none">None chosen yet</string>
|
||||
<string name="widget_config_countdown_search">Search events</string>
|
||||
<string name="widget_config_countdown_empty">No events in the coming year match.</string>
|
||||
<string name="widget_config_countdown_load_failed">Couldn\'t load your events.</string>
|
||||
<!-- A repeating event in the countdown's event list: "Next: Fri, 24 Oct · repeats" -->
|
||||
<string name="widget_config_countdown_repeats">%1$s · repeats</string>
|
||||
<string name="widget_month_day_label">Calendula month and day</string>
|
||||
<string name="widget_day_empty">No events</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>
|
||||
@@ -456,6 +515,14 @@
|
||||
<string name="widget_numbered">%1$s %2$d</string>
|
||||
<!-- A placed widget's background and theme: "Tinted · Follow app" -->
|
||||
<string name="widget_appearance_summary">%1$s · %2$s</string>
|
||||
<string name="widget_config_calendars">Calendars</string>
|
||||
<string name="widget_config_calendars_hint">The calendars this widget shows. Calendars you hid in the app stay hidden here too.</string>
|
||||
<string name="widget_config_calendars_all">All calendars</string>
|
||||
<!-- How many calendars a widget shows: "2 of 5 calendars" -->
|
||||
<plurals name="widget_config_calendars_some">
|
||||
<item quantity="one">%1$d of %2$d calendar</item>
|
||||
<item quantity="other">%1$d of %2$d calendars</item>
|
||||
</plurals>
|
||||
|
||||
<!-- Calendar filter (M3) -->
|
||||
<string name="filter_title">Calendars</string>
|
||||
@@ -554,6 +621,9 @@
|
||||
<string name="settings_past_events_sample_morning">Team review</string>
|
||||
<string name="settings_past_events_sample_midday">Lunch with Robin</string>
|
||||
<string name="settings_past_events_sample_evening">Choir practice</string>
|
||||
<!-- Stand-in event titles in the widget picker previews. Short, everyday entries. -->
|
||||
<string name="widget_preview_sample_trip">Weekend trip</string>
|
||||
<string name="widget_preview_sample_dentist">Dentist</string>
|
||||
<string name="settings_agenda_header">Agenda</string>
|
||||
<string name="settings_agenda_range">Agenda range</string>
|
||||
<string name="settings_agenda_range_hint">How far ahead the Agenda screen lists events.</string>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Colours for the static widget-picker previews. Base = brand light fallback
|
||||
(API < 31). values-v31 maps them to the Material You dynamic palette so the
|
||||
preview matches the live Glance widget; -night holds the dark variants. -->
|
||||
<!-- Colours for the static widget-picker previews (Android 14 and below; 15+
|
||||
shows the generated preview). Base = brand light fallback (API < 31).
|
||||
values-v31 maps them to the Material You dynamic palette so the preview
|
||||
matches the live Glance widget; -night holds the dark variants. -->
|
||||
<resources>
|
||||
<color name="widget_preview_surface">#FBFCFE</color>
|
||||
<color name="widget_preview_surface">#D6E4F0</color>
|
||||
<color name="widget_preview_cell">#EEF3F8</color>
|
||||
<color name="widget_preview_outside">#C2CCD6</color>
|
||||
<color name="widget_preview_on_surface">#191C1F</color>
|
||||
<color name="widget_preview_variant">#6E7479</color>
|
||||
<color name="widget_preview_primary">#3B5364</color>
|
||||
<color name="widget_preview_on_primary">#FFFFFF</color>
|
||||
<color name="widget_preview_event_blue">#039BE5</color>
|
||||
<color name="widget_preview_event_green">#33B679</color>
|
||||
<color name="widget_preview_event_purple">#8E24AA</color>
|
||||
<color name="widget_preview_event_orange">#F4511E</color>
|
||||
<color name="widget_preview_event_red">#D50000</color>
|
||||
<color name="widget_preview_on_event_light">#000000</color>
|
||||
<color name="widget_preview_on_event_dark">#FFFFFF</color>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Shared looks for the static widget-picker previews. -->
|
||||
<resources>
|
||||
<style name="WidgetPreviewHeaderButton">
|
||||
<item name="android:layout_width">24dp</item>
|
||||
<item name="android:layout_height">22dp</item>
|
||||
<item name="android:layout_marginStart">4dp</item>
|
||||
<item name="android:background">@drawable/preview_widget_cell_bg</item>
|
||||
<item name="android:gravity">center</item>
|
||||
<item name="android:textColor">@color/widget_preview_on_surface</item>
|
||||
<item name="android:textSize">12sp</item>
|
||||
</style>
|
||||
<style name="WidgetPreviewAddButton" parent="WidgetPreviewHeaderButton">
|
||||
<item name="android:layout_width">30dp</item>
|
||||
<item name="android:background">@drawable/preview_widget_today_bg</item>
|
||||
<item name="android:textColor">@color/widget_preview_on_primary</item>
|
||||
<item name="android:textSize">15sp</item>
|
||||
</style>
|
||||
<style name="WidgetPreviewWeekDay">
|
||||
<item name="android:layout_width">0dp</item>
|
||||
<item name="android:layout_height">match_parent</item>
|
||||
<item name="android:layout_weight">1</item>
|
||||
<item name="android:layout_marginHorizontal">2dp</item>
|
||||
<item name="android:background">@drawable/preview_widget_cell_bg</item>
|
||||
<item name="android:gravity">center</item>
|
||||
<item name="android:textColor">@color/widget_preview_on_surface</item>
|
||||
<item name="android:textSize">12sp</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:minWidth="110dp"
|
||||
android:minHeight="40dp"
|
||||
android:targetCellWidth="2"
|
||||
android:targetCellHeight="1"
|
||||
android:minResizeWidth="110dp"
|
||||
android:minResizeHeight="40dp"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:widgetCategory="home_screen"
|
||||
android:configure="de.jeanlucmakiola.calendula.widget.config.WidgetConfigActivity"
|
||||
android:widgetFeatures="reconfigurable"
|
||||
android:description="@string/widget_countdown_label"
|
||||
android:updatePeriodMillis="1800000"
|
||||
android:previewLayout="@layout/widget_preview_countdown"
|
||||
android:initialLayout="@layout/glance_default_loading_layout" />
|
||||
@@ -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_date_label"
|
||||
android:updatePeriodMillis="1800000"
|
||||
android:previewLayout="@layout/widget_preview_date"
|
||||
android:initialLayout="@layout/glance_default_loading_layout" />
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:minWidth="250dp"
|
||||
android:minHeight="110dp"
|
||||
android:targetCellWidth="4"
|
||||
android:targetCellHeight="2"
|
||||
android:minResizeWidth="250dp"
|
||||
android:minResizeHeight="110dp"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:widgetCategory="home_screen"
|
||||
android:configure="de.jeanlucmakiola.calendula.widget.config.WidgetConfigActivity"
|
||||
android:widgetFeatures="reconfigurable"
|
||||
android:description="@string/widget_month_day_label"
|
||||
android:updatePeriodMillis="1800000"
|
||||
android:previewLayout="@layout/widget_preview_month_day"
|
||||
android:initialLayout="@layout/glance_default_loading_layout" />
|
||||
@@ -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" />
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:minWidth="250dp"
|
||||
android:minHeight="40dp"
|
||||
android:targetCellWidth="4"
|
||||
android:targetCellHeight="1"
|
||||
android:minResizeWidth="180dp"
|
||||
android:minResizeHeight="40dp"
|
||||
android:maxResizeWidth="420dp"
|
||||
android:maxResizeHeight="230dp"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:widgetCategory="home_screen"
|
||||
android:configure="de.jeanlucmakiola.calendula.widget.config.WidgetConfigActivity"
|
||||
android:widgetFeatures="reconfigurable"
|
||||
android:description="@string/widget_today_label"
|
||||
android:updatePeriodMillis="1800000"
|
||||
android:previewLayout="@layout/widget_preview_today"
|
||||
android:initialLayout="@layout/glance_default_loading_layout" />
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:minWidth="110dp"
|
||||
android:minHeight="110dp"
|
||||
android:targetCellWidth="2"
|
||||
android:targetCellHeight="2"
|
||||
android:minResizeWidth="110dp"
|
||||
android:minResizeHeight="110dp"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:widgetCategory="home_screen"
|
||||
android:configure="de.jeanlucmakiola.calendula.widget.config.WidgetConfigActivity"
|
||||
android:widgetFeatures="reconfigurable"
|
||||
android:description="@string/widget_upcoming_label"
|
||||
android:updatePeriodMillis="1800000"
|
||||
android:previewLayout="@layout/widget_preview_upcoming"
|
||||
android:initialLayout="@layout/glance_default_loading_layout" />
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:minWidth="250dp"
|
||||
android:minHeight="40dp"
|
||||
android:targetCellWidth="4"
|
||||
android:targetCellHeight="1"
|
||||
android:minResizeWidth="180dp"
|
||||
android:minResizeHeight="40dp"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:widgetCategory="home_screen"
|
||||
android:configure="de.jeanlucmakiola.calendula.widget.config.WidgetConfigActivity"
|
||||
android:widgetFeatures="reconfigurable"
|
||||
android:description="@string/widget_week_strip_label"
|
||||
android:updatePeriodMillis="1800000"
|
||||
android:previewLayout="@layout/widget_preview_week_strip"
|
||||
android:initialLayout="@layout/glance_default_loading_layout" />
|
||||
@@ -0,0 +1,18 @@
|
||||
package de.jeanlucmakiola.calendula.widget
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.datetime.DayOfWeek
|
||||
import kotlinx.datetime.LocalDate
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class WeekOfTest {
|
||||
|
||||
@Test
|
||||
fun `the week starts on the configured day and holds today`() {
|
||||
val wednesday = LocalDate(2026, 10, 21)
|
||||
assertThat(weekOf(wednesday, DayOfWeek.MONDAY)).isEqualTo(LocalDate(2026, 10, 19)..LocalDate(2026, 10, 25))
|
||||
assertThat(weekOf(wednesday, DayOfWeek.SUNDAY)).isEqualTo(LocalDate(2026, 10, 18)..LocalDate(2026, 10, 24))
|
||||
assertThat(weekOf(wednesday, DayOfWeek.WEDNESDAY).start).isEqualTo(wednesday)
|
||||
assertThat(weekOf(wednesday, DayOfWeek.THURSDAY).start).isEqualTo(LocalDate(2026, 10, 15))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package de.jeanlucmakiola.calendula.widget.countdown
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||
import de.jeanlucmakiola.calendula.domain.EventResponse
|
||||
import kotlinx.datetime.TimeZone
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.Instant
|
||||
|
||||
class CountdownTest {
|
||||
|
||||
private val zone = TimeZone.UTC
|
||||
private val now = Instant.parse("2026-10-21T12:00:00Z")
|
||||
|
||||
private fun event(
|
||||
start: Instant,
|
||||
end: Instant = start + 1.hours,
|
||||
id: Long = 1,
|
||||
allDay: Boolean = false,
|
||||
response: EventResponse = EventResponse.Going,
|
||||
) = EventInstance(
|
||||
instanceId = id, eventId = id, calendarId = 1, title = "e$id",
|
||||
start = start, end = end, isAllDay = allDay, color = 0, location = null, response = response,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `labels step from days to hours to minutes to now`() {
|
||||
assertThat(countdownLabel(event(now + 30.seconds), now, zone)).isEqualTo(CountdownLabel(false, CountdownUnit.NOW, 0))
|
||||
assertThat(countdownLabel(event(now + 24.minutes + 10.seconds), now, zone)).isEqualTo(CountdownLabel(false, CountdownUnit.MINUTES, 25))
|
||||
assertThat(countdownLabel(event(now + 2.hours + 40.minutes), now, zone)).isEqualTo(CountdownLabel(false, CountdownUnit.HOURS, 3))
|
||||
assertThat(countdownLabel(event(now + 1.hours + 10.minutes), now, zone)).isEqualTo(CountdownLabel(false, CountdownUnit.HOURS, 1))
|
||||
// Tomorrow 23:00 is 35 h away but one calendar day.
|
||||
assertThat(countdownLabel(event(now + 35.hours), now, zone)).isEqualTo(CountdownLabel(false, CountdownUnit.DAYS, 1))
|
||||
assertThat(countdownLabel(event(now + 12.days), now, zone)).isEqualTo(CountdownLabel(false, CountdownUnit.DAYS, 12))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a running event counts to its end`() {
|
||||
val running = event(start = now - 20.minutes, end = now + 10.minutes)
|
||||
assertThat(countdownLabel(running, now, zone)).isEqualTo(CountdownLabel(true, CountdownUnit.MINUTES, 10))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the next redraw lands where the label changes`() {
|
||||
// "in 25 min" turns into "in 24 min" a minute later.
|
||||
assertThat(nextLabelChange(event(now + 25.minutes), now, zone)).isEqualTo(now + 1.minutes)
|
||||
// "in 3 h" (2 h 40 min) holds until 2 h 30 min are left.
|
||||
assertThat(nextLabelChange(event(now + 2.hours + 40.minutes), now, zone)).isEqualTo(now + 10.minutes)
|
||||
// "in 1 h" holds until the last hour, then counts minutes.
|
||||
assertThat(nextLabelChange(event(now + 1.hours + 10.minutes), now, zone)).isEqualTo(now + 10.minutes)
|
||||
// Days change at midnight.
|
||||
assertThat(nextLabelChange(event(now + 5.days), now, zone)).isEqualTo(Instant.parse("2026-10-22T00:00:00Z"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `next mode skips all-day, declined and finished events`() {
|
||||
val allDay = event(now + 1.hours, id = 1, allDay = true)
|
||||
val declined = event(now + 1.hours, id = 2, response = EventResponse.Declined)
|
||||
val over = event(now - 2.hours, now - 1.hours, id = 3)
|
||||
val later = event(now + 3.hours, id = 4)
|
||||
val running = event(now - 1.hours, now + 2.hours, id = 5)
|
||||
assertThat(pickNextEvent(listOf(allDay, declined, over, later), now)?.eventId).isEqualTo(4)
|
||||
// A running event ending before the next one starts comes first.
|
||||
assertThat(pickNextEvent(listOf(later, running), now)?.eventId).isEqualTo(5)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a pinned series follows its next occurrence`() {
|
||||
val past = event(now - 7.days)
|
||||
val next = event(now + 7.days)
|
||||
assertThat(pickPinnedOccurrence(listOf(past, next), now, zone)).isEqualTo(next)
|
||||
assertThat(pickPinnedOccurrence(listOf(past), now, zone)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an all-day event counts in local days west of UTC`() {
|
||||
val newYork = TimeZone.of("America/New_York")
|
||||
// 21 Oct, 20:00 in New York; the vacation is all day on the 23rd.
|
||||
val evening = Instant.parse("2026-10-22T00:00:00Z")
|
||||
val vacation = event(Instant.parse("2026-10-23T00:00:00Z"), Instant.parse("2026-10-24T00:00:00Z"), allDay = true)
|
||||
val counted = vacation.counted(newYork)
|
||||
assertThat(counted.start).isEqualTo(Instant.parse("2026-10-23T04:00:00Z"))
|
||||
assertThat(countdownLabel(counted, evening, newYork)).isEqualTo(CountdownLabel(false, CountdownUnit.DAYS, 2))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.jeanlucmakiola.calendula.widget.split
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.datetime.DayOfWeek
|
||||
import kotlinx.datetime.LocalDate
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class MonthGridDaysTest {
|
||||
|
||||
@Test
|
||||
fun `whole weeks from the week start cover the month and no more`() {
|
||||
// October 2026 starts on a Thursday and has 31 days.
|
||||
val october = 2026 * 12 + 9
|
||||
val monday = monthGridDays(october, DayOfWeek.MONDAY)
|
||||
assertThat(monday.first()).isEqualTo(LocalDate(2026, 9, 28))
|
||||
assertThat(monday.last()).isEqualTo(LocalDate(2026, 11, 1))
|
||||
assertThat(monday).hasSize(35)
|
||||
|
||||
// February 2027 starts on a Monday and fills exactly four weeks.
|
||||
val february = 2027 * 12 + 1
|
||||
assertThat(monthGridDays(february, DayOfWeek.MONDAY)).hasSize(28)
|
||||
assertThat(monthGridDays(february, DayOfWeek.SUNDAY)).hasSize(35)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package de.jeanlucmakiola.calendula.widget.today
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay
|
||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||
import de.jeanlucmakiola.calendula.ui.agenda.AgendaDay
|
||||
import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange
|
||||
import de.jeanlucmakiola.calendula.widget.AgendaWidgetOptions
|
||||
import de.jeanlucmakiola.calendula.widget.DaysWidgetData
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetSize
|
||||
import kotlinx.datetime.DayOfWeek
|
||||
import kotlinx.datetime.LocalDate
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Instant
|
||||
|
||||
class TodayRowsTest {
|
||||
|
||||
private val now = Instant.parse("2026-10-21T12:00:00Z")
|
||||
private val today = LocalDate(2026, 10, 21)
|
||||
private val tomorrow = LocalDate(2026, 10, 22)
|
||||
|
||||
private fun event(id: Long, start: Instant, end: Instant) = EventInstance(
|
||||
instanceId = id, eventId = id, calendarId = 1, title = "e$id",
|
||||
start = start, end = end, isAllDay = false, color = 0, location = null,
|
||||
)
|
||||
|
||||
private fun data(past: PastEventDisplay, vararg days: AgendaDay) = DaysWidgetData.Ready(
|
||||
today = today,
|
||||
firstDay = today,
|
||||
lastDay = tomorrow,
|
||||
days = days.toList(),
|
||||
weekStart = DayOfWeek.MONDAY,
|
||||
is24Hour = true,
|
||||
soften = true,
|
||||
options = AgendaWidgetOptions(AgendaRange.Day, WidgetSize.SMALL, true, past),
|
||||
now = now,
|
||||
)
|
||||
|
||||
private val ended = event(1, now - 3.hours, now - 1.hours)
|
||||
private val later = event(2, now + 1.hours, now + 2.hours)
|
||||
private val next = event(3, now + 22.hours, now + 23.hours)
|
||||
|
||||
@Test
|
||||
fun `an event still to come lists today, finished ones as the past setting says`() {
|
||||
val days = arrayOf(AgendaDay(today, listOf(ended, later)), AgendaDay(tomorrow, listOf(next)))
|
||||
assertThat(todayRows(data(PastEventDisplay.DIM, *days))).isEqualTo(TodayRows.Today(listOf(ended, later)))
|
||||
assertThat(todayRows(data(PastEventDisplay.HIDE, *days))).isEqualTo(TodayRows.Today(listOf(later)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `once today is over it looks ahead to tomorrow`() {
|
||||
val over = data(PastEventDisplay.SHOW, AgendaDay(today, listOf(ended)), AgendaDay(tomorrow, listOf(next)))
|
||||
assertThat(todayRows(over)).isEqualTo(TodayRows.NothingLeft(listOf(next)))
|
||||
assertThat(todayRows(data(PastEventDisplay.SHOW))).isEqualTo(TodayRows.NothingLeft(emptyList()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package de.jeanlucmakiola.calendula.widget.upcoming
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay
|
||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||
import de.jeanlucmakiola.calendula.ui.agenda.AgendaDay
|
||||
import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange
|
||||
import de.jeanlucmakiola.calendula.widget.AgendaWidgetOptions
|
||||
import de.jeanlucmakiola.calendula.widget.DaysWidgetData
|
||||
import de.jeanlucmakiola.calendula.widget.WidgetSize
|
||||
import kotlinx.datetime.DayOfWeek
|
||||
import kotlinx.datetime.LocalDate
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Instant
|
||||
|
||||
class UpcomingEventsTest {
|
||||
|
||||
private val now = Instant.parse("2026-10-21T12:00:00Z")
|
||||
|
||||
private fun event(id: Long, start: Instant, end: Instant) = EventInstance(
|
||||
instanceId = id, eventId = id, calendarId = 1, title = "e$id",
|
||||
start = start, end = end, isAllDay = false, color = 0, location = null,
|
||||
)
|
||||
|
||||
private fun data(vararg days: AgendaDay) = DaysWidgetData.Ready(
|
||||
today = LocalDate(2026, 10, 21),
|
||||
firstDay = LocalDate(2026, 10, 21),
|
||||
lastDay = LocalDate(2026, 10, 27),
|
||||
days = days.toList(),
|
||||
weekStart = DayOfWeek.MONDAY,
|
||||
is24Hour = true,
|
||||
soften = true,
|
||||
options = AgendaWidgetOptions(AgendaRange.Week, WidgetSize.SMALL, true, PastEventDisplay.SHOW),
|
||||
now = now,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `ended events drop out and a multi-day one lists once, on its first day`() {
|
||||
val ended = event(1, now - 3.hours, now - 1.hours)
|
||||
val running = event(2, now - 1.hours, now + 30.hours)
|
||||
val later = event(3, now + 2.hours, now + 3.hours)
|
||||
val today = AgendaDay(LocalDate(2026, 10, 21), listOf(ended, running, later))
|
||||
val tomorrow = AgendaDay(LocalDate(2026, 10, 22), listOf(running))
|
||||
|
||||
val upcoming = upcomingEvents(data(today, tomorrow))
|
||||
|
||||
assertThat(upcoming.map { it.second.instanceId }).containsExactly(2L, 3L).inOrder()
|
||||
assertThat(upcoming.first().first).isEqualTo(LocalDate(2026, 10, 21))
|
||||
}
|
||||
}
|
||||
+1
-1
Submodule floret-kit updated: f2ffaed925...5716402e72
Reference in New Issue
Block a user