Agenda widget: classic style and background opacity (#489)

This commit is contained in:
makiolaj committed 2026-11-09 18:31:12 +01:00
1 parent 9ccc303a2a
commit fffb033e39
9 files changed
+305 -63

No files matched your search

@@ -4,6 +4,7 @@ import android.content.Context
import androidx.datastore.preferences.core.MutablePreferences
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.stringSetPreferencesKey
import androidx.glance.GlanceId
@@ -26,14 +27,20 @@ enum class WidgetTheme { FOLLOW_APP, LIGHT, DARK }
data class WidgetAppearance(
val background: WidgetBackground = WidgetBackground.TINTED,
val theme: WidgetTheme = WidgetTheme.FOLLOW_APP,
/** How opaque a Solid or Tinted fill is (#489). */
val opacity: Float = 1f,
)
/** The agenda widget's look: the 2.24 day cards, or v2.23's plain day headers and stripes (#489). */
enum class AgendaWidgetStyle { CARDS, CLASSIC }
/** What the agenda widget lists, per placed widget. */
data class AgendaWidgetOptions(
val range: AgendaRange,
val textSize: WidgetSize,
val showToday: Boolean,
val pastDisplay: PastEventDisplay,
val style: AgendaWidgetStyle = AgendaWidgetStyle.CARDS,
)
internal fun WidgetTheme.resolve(app: ThemeMode): WidgetThemeMode = when (this) {
@@ -48,6 +55,8 @@ internal fun WidgetTheme.resolve(app: ThemeMode): WidgetThemeMode = when (this)
private val BackgroundKey = stringPreferencesKey("cfg_background")
private val ThemeKey = stringPreferencesKey("cfg_theme")
private val OpacityKey = floatPreferencesKey("cfg_opacity")
private val AgendaStyleKey = stringPreferencesKey("cfg_agenda_style")
private val AgendaRangeKey = stringPreferencesKey("cfg_agenda_range")
private val TextSizeKey = stringPreferencesKey("cfg_text_size")
private val ShowTodayKey = booleanPreferencesKey("cfg_show_today")
@@ -58,6 +67,7 @@ internal val Preferences.widgetAppearance: WidgetAppearance
get() = WidgetAppearance(
background = this[BackgroundKey].toEnum(WidgetBackground.TINTED),
theme = this[ThemeKey].toEnum(WidgetTheme.FOLLOW_APP),
opacity = this[OpacityKey] ?: 1f,
)
/**
@@ -70,6 +80,7 @@ internal fun Preferences.agendaWidgetOptions(defaults: AgendaWidgetOptions) = Ag
textSize = this[TextSizeKey].toEnum(defaults.textSize),
showToday = this[ShowTodayKey] ?: defaults.showToday,
pastDisplay = this[PastDisplayKey].toEnum(defaults.pastDisplay),
style = this[AgendaStyleKey].toEnum(defaults.style),
)
/** Whether this widget has agenda options of its own, rather than following the app's. */
@@ -78,6 +89,7 @@ internal val Preferences.hasAgendaOptions: Boolean get() = this[AgendaRangeKey]
internal fun MutablePreferences.putAppearance(appearance: WidgetAppearance) {
this[BackgroundKey] = appearance.background.name
this[ThemeKey] = appearance.theme.name
this[OpacityKey] = appearance.opacity
}
internal fun MutablePreferences.putAgendaOptions(options: AgendaWidgetOptions) {
@@ -85,6 +97,7 @@ internal fun MutablePreferences.putAgendaOptions(options: AgendaWidgetOptions) {
this[TextSizeKey] = options.textSize.name
this[ShowTodayKey] = options.showToday
this[PastDisplayKey] = options.pastDisplay.name
this[AgendaStyleKey] = options.style.name
}
/**
@@ -126,6 +126,7 @@ sealed interface AgendaWidgetData {
/** "Always show today" (#35): anchor today at the top even with no events left. */
val showToday: Boolean,
val textSize: WidgetSize,
val style: AgendaWidgetStyle = AgendaWidgetStyle.CARDS,
/** Snapshot instant the data was read at, for "has this event ended?" tests. */
val now: Instant,
) : AgendaWidgetData
@@ -184,6 +185,7 @@ internal suspend fun Context.loadAgendaWidgetData(
pastDisplay = options.pastDisplay,
showToday = options.showToday,
textSize = options.textSize,
style = options.style,
now = Clock.System.now(),
)
}
@@ -26,7 +26,7 @@ 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 }
enum class WidgetOption { STYLE, RANGE, TEXT_SIZE, PAST_DISPLAY, SHOW_TODAY, CALENDARS }
/**
* Every widget the app ships, told apart by their receiver: what redraws,
@@ -42,7 +42,14 @@ enum class WidgetKind(
AGENDA(
AgendaWidgetReceiver::class.java,
R.string.widget_agenda_label,
setOf(WidgetOption.RANGE, WidgetOption.TEXT_SIZE, WidgetOption.PAST_DISPLAY, WidgetOption.SHOW_TODAY, WidgetOption.CALENDARS),
setOf(
WidgetOption.STYLE,
WidgetOption.RANGE,
WidgetOption.TEXT_SIZE,
WidgetOption.PAST_DISPLAY,
WidgetOption.SHOW_TODAY,
WidgetOption.CALENDARS,
),
DpSize(260.dp, 280.dp),
),
MONTH(
@@ -34,7 +34,7 @@ fun CalendulaGlanceTheme(mode: WidgetThemeMode, content: @Composable () -> Unit)
@Composable
internal fun CalendulaWidgetRoot(appearance: WidgetAppearance, appTheme: ThemeMode, content: @Composable () -> Unit) {
CalendulaGlanceTheme(appearance.theme.resolve(appTheme)) {
WidgetSurface(background = appearance.background, content = content)
WidgetSurface(background = appearance.background, opacity = appearance.opacity, content = content)
}
}
@@ -12,6 +12,14 @@ internal val ROW_H_PAD = 12.dp
/** Space between the date-pill gutter and a day's card. */
internal val GUTTER_GAP = 6.dp
/** The classic style's row inset, colour stripe and the gap after it (#489). */
internal val CLASSIC_ROW_H_PAD = 4.dp
internal val STRIPE_WIDTH = 5.dp
internal val STRIPE_GAP = 10.dp
/** Where the classic style's event text starts, so its placeholder lines up with the titles. */
internal val TEXT_INDENT = CLASSIC_ROW_H_PAD + STRIPE_WIDTH + STRIPE_GAP
/**
* Every size the agenda widget varies by [WidgetSize]. Values that genuinely
* shouldn't grow (the horizontal row constants above, corner radii) stay
@@ -29,6 +37,8 @@ internal data class AgendaMetrics(
val rowVPad: Dp, // event row vertical padding
val dayGap: Dp, // space between two days' cards
val monthLabel: TextUnit, // month name above a new month's first day
val dayHeader: TextUnit, // classic style's day label ("Today · …")
val dayHeaderTopPad: Dp, // classic style's space above a day label
) {
/** One event row's height, shared by the date pill beside it and the placeholder row. */
val rowHeight: Dp get() = contentH + rowVPad * 2
@@ -61,6 +71,8 @@ private val SMALL_METRICS = AgendaMetrics(
rowVPad = 4.dp,
dayGap = 8.dp,
monthLabel = 12.sp,
dayHeader = 13.sp,
dayHeaderTopPad = 10.dp,
)
private val MEDIUM_METRICS = AgendaMetrics(
@@ -75,6 +87,8 @@ private val MEDIUM_METRICS = AgendaMetrics(
rowVPad = 5.dp,
dayGap = 9.dp,
monthLabel = 14.sp,
dayHeader = 14.sp, // M3 Title Small
dayHeaderTopPad = 11.dp,
)
private val LARGE_METRICS = AgendaMetrics(
@@ -89,6 +103,8 @@ private val LARGE_METRICS = AgendaMetrics(
rowVPad = 6.dp,
dayGap = 10.dp,
monthLabel = 14.sp,
dayHeader = 16.sp, // M3 Title Medium
dayHeaderTopPad = 12.dp,
)
private val EXTRA_LARGE_METRICS = AgendaMetrics(
@@ -103,6 +119,8 @@ private val EXTRA_LARGE_METRICS = AgendaMetrics(
rowVPad = 8.dp,
dayGap = 12.dp,
monthLabel = 16.sp,
dayHeader = 18.sp, // †
dayHeaderTopPad = 14.dp,
)
/** Indexed by [WidgetSize.ordinal] so lookup allocates nothing per recomposition. */
@@ -11,6 +11,7 @@ import de.jeanlucmakiola.calendula.widget.glanceDeclinedDecoration
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.glance.GlanceId
import androidx.glance.GlanceModifier
@@ -56,6 +57,11 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarView
import de.jeanlucmakiola.calendula.ui.common.formatCalendarTitle
import de.jeanlucmakiola.floret.locale.localizedDateFormatter
import de.jeanlucmakiola.calendula.ui.common.eventFill
import de.jeanlucmakiola.calendula.ui.common.eventAccent
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
import de.jeanlucmakiola.calendula.widget.AgendaWidgetStyle
import de.jeanlucmakiola.floret.glance.LocalWidgetBackground
import de.jeanlucmakiola.floret.glance.WidgetBackground
import de.jeanlucmakiola.calendula.widget.WidgetDimFillAlpha
import de.jeanlucmakiola.calendula.widget.widgetEventInk
import de.jeanlucmakiola.calendula.ui.common.SECONDARY_INK_ALPHA
@@ -220,59 +226,216 @@ private fun AgendaRows(
}
}
}.take(MAX_AGENDA_ROWS)
// Each day is one card of grouped rows (#71), with its date pill
// beside the first row.
val positions = dayPositions(rows)
val cardColor = widgetCardColor()
val item: @Composable (Int) -> Unit = { index ->
val row = rows[index]
val position = positions[index]
val first = position == Position.Top || position == Position.Alone
val dayRow: @Composable (GlanceModifier) -> Unit = { modifier ->
Row(modifier = modifier.fillMaxWidth()) {
Box(modifier = GlanceModifier.width(metrics.pillWidth)) {
if (first) DatePill(row.date, data.today, cardColor, metrics)
}
Spacer(GlanceModifier.width(GUTTER_GAP))
Box(modifier = GlanceModifier.defaultWeight()) {
val rowColor = when (row) {
is AgendaRow.Event -> row.fill(data).let { themedColor(day = it.day, night = it.night) }
is AgendaRow.Placeholder -> cardColor
}
WidgetGroupedRow(position = position, color = rowColor) {
AgendaRowContent(row, data, metrics)
}
}
}
}
val gap = GlanceModifier.padding(top = if (first && index > 0) metrics.dayGap else 0.dp)
// The pills carry no month, so a new month gets its name above its first day.
if (first && index > 0 && row.date.month != rows[index - 1].date.month) {
Column(modifier = gap.fillMaxWidth()) {
MonthLabel(row.date, data.today, metrics)
dayRow(GlanceModifier)
}
} else {
dayRow(gap)
}
}
if (preview) {
Column(modifier = GlanceModifier.fillMaxSize()) {
repeat(minOf(rows.size, PREVIEW_ROWS)) { item(it) }
}
} else {
LazyColumn(modifier = GlanceModifier.fillMaxSize()) {
items(rows.size) { item(it) }
}
when (data.style) {
AgendaWidgetStyle.CARDS -> CardRows(rows, data, metrics, preview)
AgendaWidgetStyle.CLASSIC -> ClassicRows(rows, data, metrics, preview)
}
}
}
/** Where each row sits among the rows of its day, for the grouped-card shapes. */
private fun dayPositions(rows: List<AgendaRow>): List<Position> = buildList {
/** Each day is one card of grouped rows (#71), with its date pill beside the first row. */
@Composable
private fun CardRows(rows: List<AgendaRow>, data: AgendaWidgetData.Ready, metrics: AgendaMetrics, preview: Boolean) {
val positions = dayPositions(rows.map { it.date })
val cardColor = widgetCardColor()
val item: @Composable (Int) -> Unit = { index ->
val row = rows[index]
val position = positions[index]
val first = position == Position.Top || position == Position.Alone
val dayRow: @Composable (GlanceModifier) -> Unit = { modifier ->
Row(modifier = modifier.fillMaxWidth()) {
Box(modifier = GlanceModifier.width(metrics.pillWidth)) {
if (first) DatePill(row.date, data.today, cardColor, metrics)
}
Spacer(GlanceModifier.width(GUTTER_GAP))
Box(modifier = GlanceModifier.defaultWeight()) {
val rowColor = when (row) {
is AgendaRow.Event -> row.fill(data).let { themedColor(day = it.day, night = it.night) }
is AgendaRow.Placeholder -> cardColor
}
WidgetGroupedRow(position = position, color = rowColor) {
AgendaRowContent(row, data, metrics)
}
}
}
}
val gap = GlanceModifier.padding(top = if (first && index > 0) metrics.dayGap else 0.dp)
// The pills carry no month, so a new month gets its name above its first day.
if (first && index > 0 && row.date.month != rows[index - 1].date.month) {
Column(modifier = gap.fillMaxWidth()) {
MonthLabel(row.date, data.today, metrics)
dayRow(GlanceModifier)
}
} else {
dayRow(gap)
}
}
RowList(rows.size, preview, item)
}
/** [count] rows of [item]: a plain column in a [preview], else a [LazyColumn]. */
@Composable
private fun RowList(count: Int, preview: Boolean, item: @Composable (Int) -> Unit) {
if (preview) {
Column(modifier = GlanceModifier.fillMaxSize()) {
repeat(minOf(count, PREVIEW_ROWS)) { item(it) }
}
} else {
LazyColumn(modifier = GlanceModifier.fillMaxSize()) {
items(count) { item(it) }
}
}
}
/** A classic list entry: a day's label line, or one of its rows. */
private sealed interface ClassicItem {
val date: LocalDate
data class Header(override val date: LocalDate) : ClassicItem
data class Entry(val row: AgendaRow) : ClassicItem {
override val date: LocalDate get() = row.date
}
}
/**
* v2.23's look (#489): a label line per day over plain rows with a colour
* stripe. On a transparent root each day sits on its own backing card, so the
* text stays readable over the wallpaper.
*/
@Composable
private fun ClassicRows(rows: List<AgendaRow>, data: AgendaWidgetData.Ready, metrics: AgendaMetrics, preview: Boolean) {
val items = buildList {
rows.forEachIndexed { index, row ->
if (index == 0 || row.date != rows[index - 1].date) add(ClassicItem.Header(row.date))
add(ClassicItem.Entry(row))
}
}
val backed = LocalWidgetBackground.current == WidgetBackground.TRANSPARENT
val positions = dayPositions(items.map { it.date })
val item: @Composable (Int) -> Unit = { index ->
val entry = items[index]
val content: @Composable () -> Unit = {
when (entry) {
is ClassicItem.Header -> DayHeaderRow(
date = entry.date,
today = data.today,
topPad = if (backed) CLASSIC_CARD_TOP_PAD else metrics.dayHeaderTopPad,
metrics = metrics,
)
is ClassicItem.Entry -> when (val row = entry.row) {
is AgendaRow.Placeholder -> ClassicPlaceholderRow(row.date, metrics)
is AgendaRow.Event -> ClassicEventRow(row, data, metrics)
}
}
}
if (backed) {
val gap = if (entry is ClassicItem.Header && index > 0) metrics.dayGap else 0.dp
WidgetGroupedRow(
position = positions[index],
modifier = GlanceModifier.padding(top = gap),
color = GlanceTheme.colors.widgetBackground,
content = content,
)
} else {
content()
}
}
RowList(items.size, preview, item)
}
/** A classic day label, "Today · Wed, 17 Jun"; opens that day in the app. */
@Composable
private fun DayHeaderRow(date: LocalDate, today: LocalDate, topPad: Dp, metrics: AgendaMetrics) {
val context = androidx.glance.LocalContext.current
Text(
text = agendaDayLabel(context, date, today),
maxLines = 1,
style = TextStyle(
color = if (date == today) GlanceTheme.colors.primary else GlanceTheme.colors.onSurfaceVariant,
fontSize = metrics.dayHeader,
fontWeight = FontWeight.Medium,
),
modifier = GlanceModifier
.fillMaxWidth()
.padding(start = 8.dp, end = 8.dp, top = topPad, bottom = metrics.rowVPad)
.clickable(actionStartActivity(MainActivity.openDateIntent(context, date, CalendarView.Agenda))),
)
}
/** "No more events today" as a muted line under an anchored, event-less today (#35), indented to the titles. */
@Composable
private fun ClassicPlaceholderRow(date: LocalDate, metrics: AgendaMetrics) {
val context = androidx.glance.LocalContext.current
Text(
text = context.getString(R.string.agenda_no_more_today),
maxLines = 1,
style = TextStyle(color = GlanceTheme.colors.onSurfaceVariant, fontSize = metrics.placeholder),
modifier = GlanceModifier
.fillMaxWidth()
.padding(start = TEXT_INDENT, end = 8.dp, top = 2.dp, bottom = metrics.rowVPad + 2.dp)
.clickable(actionStartActivity(MainActivity.openDateIntent(context, date, CalendarView.Agenda))),
)
}
/** A classic event row: the calendar's colour stripe beside the title and time; a dimmed one fades both. */
@Composable
private fun ClassicEventRow(row: AgendaRow.Event, data: AgendaWidgetData.Ready, metrics: AgendaMetrics) {
val context = androidx.glance.LocalContext.current
val event = row.event
val dimmed = data.pastDisplay == PastEventDisplay.DIM && event.hasEnded(data.now)
fun accent(dark: Boolean) = eventAccent(event.color, dark, data.soften).let {
if (dimmed) it.copy(alpha = EventDimAlpha) else it
}
Row(
modifier = GlanceModifier
.fillMaxWidth()
.padding(horizontal = CLASSIC_ROW_H_PAD, vertical = metrics.rowVPad)
.clickable(actionStartActivity(event.openIntent(context))),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = GlanceModifier
.width(STRIPE_WIDTH)
.height(metrics.contentH)
.cornerRadius(3.dp)
.background(themedColor(day = accent(dark = false), night = accent(dark = true))),
) {}
Spacer(GlanceModifier.width(STRIPE_GAP))
Column(modifier = GlanceModifier.defaultWeight()) {
Text(
text = event.title.ifBlank { context.getString(R.string.event_untitled) },
maxLines = 1,
style = TextStyle(
color = if (dimmed) GlanceTheme.colors.onSurfaceVariant else GlanceTheme.colors.onSurface,
fontSize = metrics.eventTitle,
textDecoration = glanceDeclinedDecoration(event.isCrossedOut),
),
)
Text(
text = eventTimeSummary(context, event, row.date, data.is24Hour),
maxLines = 1,
style = TextStyle(color = GlanceTheme.colors.onSurfaceVariant, fontSize = metrics.eventTime),
)
}
}
}
/** Space above a day's label on its own backing card. */
private val CLASSIC_CARD_TOP_PAD = 8.dp
private fun EventInstance.openIntent(context: Context) = MainActivity.openEventIntent(
context = context,
eventId = eventId,
beginMillis = start.toEpochMilliseconds(),
endMillis = end.toEpochMilliseconds(),
source = CalendarView.Agenda,
)
/** Where each row sits among the rows of its day, from each row's date, for the grouped-card shapes. */
private fun dayPositions(dates: List<LocalDate>): List<Position> = buildList {
var start = 0
for (end in 1..rows.size) {
if (end == rows.size || rows[end].date != rows[start].date) {
for (end in 1..dates.size) {
if (end == dates.size || dates[end] != dates[start]) {
for (i in start until end) add(positionOf(i - start, end - start))
start = end
}
@@ -421,17 +584,7 @@ private fun EventRow(
.fillMaxWidth()
.height(metrics.rowHeight)
.padding(horizontal = ROW_H_PAD)
.clickable(
actionStartActivity(
MainActivity.openEventIntent(
context = context,
eventId = event.eventId,
beginMillis = event.start.toEpochMilliseconds(),
endMillis = event.end.toEpochMilliseconds(),
source = CalendarView.Agenda,
),
),
),
.clickable(actionStartActivity(event.openIntent(context))),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
@@ -69,6 +69,8 @@ import de.jeanlucmakiola.floret.glance.GlanceWidgetPreview
import de.jeanlucmakiola.floret.glance.WallpaperWindow
import de.jeanlucmakiola.floret.glance.WidgetBackground
import kotlin.math.roundToInt
import de.jeanlucmakiola.floret.components.GroupedSliderRow
import de.jeanlucmakiola.calendula.widget.AgendaWidgetStyle
private val PreviewMaxHeight = 280.dp
@@ -236,6 +238,18 @@ private fun ConfigOptions(
onSelect = { onAppearance(state.appearance.copy(background = it)) },
position = Position.Top,
)
if (state.appearance.background != WidgetBackground.TRANSPARENT) {
val opacity = state.appearance.opacity
GroupedSliderRow(
title = stringResource(R.string.widget_config_opacity),
value = opacity,
onValueChange = { onAppearance(state.appearance.copy(opacity = it)) },
position = Position.Middle,
valueLabel = stringResource(R.string.widget_config_opacity_value, (opacity * 100).roundToInt()),
valueRange = MIN_OPACITY..1f,
steps = OPACITY_STEPS,
)
}
GroupedChoiceRow(
title = stringResource(R.string.settings_theme),
options = WidgetTheme.entries,
@@ -265,6 +279,7 @@ private fun ContentOptions(
onCalendarShown: (Long, Boolean) -> Unit,
) {
val options = state.draft.content
var showStyle by rememberSaveable { mutableStateOf(false) }
var showRange by rememberSaveable { mutableStateOf(false) }
var showSize by rememberSaveable { mutableStateOf(false) }
var showPast by rememberSaveable { mutableStateOf(false) }
@@ -277,6 +292,12 @@ private fun ContentOptions(
GroupedSectionHeader(stringResource(R.string.widget_config_content))
rows.forEach { option ->
when (option) {
WidgetOption.STYLE -> GroupedRow(
title = stringResource(R.string.widget_config_style),
summary = agendaStyleLabel(options.style),
position = position(option),
onClick = { showStyle = true },
)
WidgetOption.RANGE -> GroupedRow(
title = stringResource(R.string.widget_config_range),
summary = agendaRangeLabel(options.range),
@@ -319,6 +340,17 @@ private fun ContentOptions(
)
}
if (showStyle) {
OptionPicker(
title = stringResource(R.string.widget_config_style),
predictiveBack = true,
options = AgendaWidgetStyle.entries,
selected = options.style,
label = { agendaStyleLabel(it) },
onSelect = { onChange(options.copy(style = it)) },
onDismiss = { showStyle = false },
)
}
if (showRange) {
AgendaRangePicker(
title = stringResource(R.string.widget_config_range),
@@ -356,6 +388,18 @@ private fun ContentOptions(
}
}
@Composable
private fun agendaStyleLabel(style: AgendaWidgetStyle): String = stringResource(
when (style) {
AgendaWidgetStyle.CARDS -> R.string.widget_config_style_cards
AgendaWidgetStyle.CLASSIC -> R.string.widget_config_style_classic
},
)
/** The faintest fill the opacity slider allows, and its 10% steps between that and opaque. */
private const val MIN_OPACITY = 0.2f
private const val OPACITY_STEPS = 7
@Composable
internal fun backgroundLabel(background: WidgetBackground): String = stringResource(
when (background) {
+5
View File
@@ -517,6 +517,11 @@
<string name="widget_config_background_tinted">Tinted</string>
<string name="widget_config_background_transparent">Transparent</string>
<string name="widget_config_theme_follow_app">Follow app</string>
<string name="widget_config_opacity">Opacity</string>
<string name="widget_config_opacity_value">%1$d%%</string>
<string name="widget_config_style">Style</string>
<string name="widget_config_style_cards">Cards</string>
<string name="widget_config_style_classic">Classic</string>
<string name="widget_config_content">Content</string>
<string name="widget_config_range">Time range</string>
<string name="widget_config_text_size">Text size</string>