feat(widgets): let each widget pick its theme and wallpaper colours
Every widget instance can now follow the system or be pinned to Light or Dark, and on Android 12+ turn the wallpaper's colours off in favour of the app's own palette. Both are set in the widget's config screen and stored in its own Glance state; an instance without them keeps today's look. Glance content takes them through WidgetTheme. The plain-RemoteViews parts (the analog and digital faces, readouts and chronometers) go through a new WidgetPalette, which resolves the widget_* palette under a forced day/night and sets concrete colours, or day/night pairs, on the views. The app's own tones are now widget_app_*, so they stay reachable on API 31+. The analog and digital clocks are re-coloured on API 31+ only: below that AnalogClock's hands and a shape's background tint can't be set through RemoteViews, so their config screens don't offer it. The next-alarm and timer widgets gain a config screen for this (configuration_optional, so placing one doesn't stop there). The analog, stopwatch and quick-timer config screens become settings screens with an OK, like the digital clock's, since they now hold more than one choice. Background opacity is left out: most faces are a shape with text in its container colour rather than a card, so there is no background to fade without losing contrast against the wallpaper. Closes #6
This commit is contained in:
@@ -318,6 +318,25 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- The next-alarm and timer widgets' per-instance colours: the same
|
||||
APPWIDGET_CONFIGURE contract as the four above. -->
|
||||
<activity
|
||||
android:name=".ui.widget.config.NextAlarmConfigActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.Clockula">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.widget.config.TimerConfigActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.Clockula">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- The quick-timer widget's one tap: creates and starts a timer at
|
||||
its configured duration. A broadcast, not a service start or an
|
||||
activity, for the same reason as `TimerActionReceiver` (M11 D16). -->
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package de.jeanlucmakiola.clockula.domain.widget
|
||||
|
||||
/** A widget instance's light/dark: follow the launcher, or pin one. */
|
||||
enum class WidgetColorTheme { SYSTEM, LIGHT, DARK }
|
||||
|
||||
/**
|
||||
* A widget instance's colours, set in its config screen and stored in its
|
||||
* own Glance state. The defaults are what every widget did before this was
|
||||
* configurable — follow the launcher's light/dark and, on Android 12+, the
|
||||
* wallpaper — so an instance with nothing stored looks exactly as it did.
|
||||
*
|
||||
* There is deliberately no free colour choice: every combination here is
|
||||
* one of two tested palettes in one of its two tones, so text always keeps
|
||||
* its contrast.
|
||||
*/
|
||||
data class WidgetColors(
|
||||
val theme: WidgetColorTheme = WidgetColorTheme.SYSTEM,
|
||||
val wallpaperColors: Boolean = true,
|
||||
)
|
||||
|
||||
/** Unset or unknown values fall back to [WidgetColors]' defaults. */
|
||||
fun parseWidgetColors(theme: String?, wallpaperColors: Boolean?): WidgetColors = WidgetColors(
|
||||
theme = WidgetColorTheme.entries.firstOrNull { it.name == theme } ?: WidgetColorTheme.SYSTEM,
|
||||
wallpaperColors = wallpaperColors ?: true,
|
||||
)
|
||||
|
||||
/** Where a widget's `widget_*` palette colours come from, once its [WidgetColors] meet the device. */
|
||||
sealed interface WidgetPaletteMode {
|
||||
|
||||
/** The launcher resolves the palette resources against its own day/night and wallpaper — the default look. */
|
||||
data object Launcher : WidgetPaletteMode
|
||||
|
||||
/** The app's own palette, with day/night still left to the launcher. */
|
||||
data object AppDayNight : WidgetPaletteMode
|
||||
|
||||
/** One tone, [dark] or light, of the wallpaper's palette when [dynamic], else of the app's. */
|
||||
data class Pinned(val dark: Boolean, val dynamic: Boolean) : WidgetPaletteMode
|
||||
}
|
||||
|
||||
/**
|
||||
* [WidgetColors] resolved for a device: [dynamicAvailable] is Android 12+,
|
||||
* where wallpaper colours exist. Below that the launcher's palette already
|
||||
* *is* the app's, so "wallpaper colours off" changes nothing there.
|
||||
*/
|
||||
fun WidgetColors.paletteMode(dynamicAvailable: Boolean): WidgetPaletteMode {
|
||||
val dynamic = wallpaperColors && dynamicAvailable
|
||||
return when (theme) {
|
||||
WidgetColorTheme.SYSTEM -> if (dynamic || !dynamicAvailable) WidgetPaletteMode.Launcher else WidgetPaletteMode.AppDayNight
|
||||
WidgetColorTheme.LIGHT -> WidgetPaletteMode.Pinned(dark = false, dynamic = dynamic)
|
||||
WidgetColorTheme.DARK -> WidgetPaletteMode.Pinned(dark = true, dynamic = dynamic)
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import android.graphics.Typeface
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.text.format.DateFormat
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
@@ -28,6 +29,7 @@ import androidx.glance.layout.size
|
||||
import de.jeanlucmakiola.clockula.R
|
||||
import de.jeanlucmakiola.clockula.domain.widget.AnalogClockStyle
|
||||
import de.jeanlucmakiola.clockula.domain.widget.ClockWidgetTapTarget
|
||||
import de.jeanlucmakiola.clockula.domain.widget.WidgetColors
|
||||
import de.jeanlucmakiola.clockula.domain.widget.parseAnalogClockStyle
|
||||
import de.jeanlucmakiola.clockula.domain.widget.parseClockWidgetTapTarget
|
||||
import de.jeanlucmakiola.clockula.widget.WIDGET_ANALOG_STYLE_KEY
|
||||
@@ -48,7 +50,8 @@ fun AnalogClockWidgetRoot() {
|
||||
* floating directly on the home screen, not on a card, the way a platform
|
||||
* clock widget looks. Its colours are the analog palette resources
|
||||
* (`res/values/colors.xml` and its night/v31 variants), so it follows the launcher's day/night and, on
|
||||
* API 31+, the wallpaper by itself. A tap opens [tapTarget]'s tab.
|
||||
* API 31+, the wallpaper by itself — unless the instance's [LocalWidgetColors]
|
||||
* say otherwise. A tap opens [tapTarget]'s tab.
|
||||
*/
|
||||
@Composable
|
||||
fun AnalogClockWidgetContent(style: AnalogClockStyle, tapTarget: ClockWidgetTapTarget) {
|
||||
@@ -73,7 +76,7 @@ private fun AnalogFace(style: AnalogClockStyle) {
|
||||
val side = minOf(size.width, size.height)
|
||||
Box(modifier = GlanceModifier.size(side)) {
|
||||
AndroidRemoteViews(
|
||||
remoteViews = analogClockViews(context, style, side.value),
|
||||
remoteViews = analogClockViews(context, style, side.value, LocalWidgetColors.current),
|
||||
modifier = GlanceModifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
@@ -132,6 +135,14 @@ private fun AnalogClockStyle.seconds(): AnalogSeconds = when (this) {
|
||||
AnalogClockStyle.SCALLOP -> AnalogSeconds.DotAndDate(R.drawable.widget_analog_second_dot_scallop)
|
||||
}
|
||||
|
||||
/** The palette role a style's dial drawable is filled with (`res/drawable/widget_analog_dial_*`). */
|
||||
private fun AnalogClockStyle.dialRole(): WidgetRole = when (this) {
|
||||
AnalogClockStyle.ROUND, AnalogClockStyle.NUMERAL, AnalogClockStyle.BADGE,
|
||||
AnalogClockStyle.SCALLOP -> WidgetRole.ANALOG_DIAL_FILL
|
||||
AnalogClockStyle.COOKIE, AnalogClockStyle.CLOVER -> WidgetRole.ANALOG_DIAL_PRIMARY
|
||||
AnalogClockStyle.PETAL, AnalogClockStyle.SUNNY, AnalogClockStyle.SQUARE -> WidgetRole.ANALOG_DIAL_TERTIARY
|
||||
}
|
||||
|
||||
private fun AnalogClockStyle.overlay(): AnalogOverlay = when (this) {
|
||||
AnalogClockStyle.NUMERAL, AnalogClockStyle.CLOVER -> AnalogOverlay.NUMERALS
|
||||
AnalogClockStyle.BADGE -> AnalogOverlay.DAY
|
||||
@@ -146,8 +157,19 @@ private fun AnalogClockStyle.overlay(): AnalogOverlay = when (this) {
|
||||
* inflate these same views in-app), so a preview is the real widget.
|
||||
* Numerals, badge and their insets are fractions of [sideDp], so they grow
|
||||
* and shrink with the dial instead of staying at a fixed sp/dp.
|
||||
*
|
||||
* [colors] other than the defaults re-colour the face ([recolor]) on API
|
||||
* 31+ only: below that `AnalogClock`'s hands can be neither swapped nor
|
||||
* tinted, so a pinned tone would leave them in the launcher's, and the
|
||||
* config screen doesn't offer it.
|
||||
*/
|
||||
internal fun analogClockViews(context: Context, style: AnalogClockStyle, sideDp: Float): RemoteViews {
|
||||
internal fun analogClockViews(
|
||||
context: Context,
|
||||
style: AnalogClockStyle,
|
||||
sideDp: Float,
|
||||
colors: WidgetColors = WidgetColors(),
|
||||
): RemoteViews {
|
||||
val palette = WidgetPalette(context, colors)
|
||||
val overlay = style.overlay()
|
||||
val layoutRes = when (overlay) {
|
||||
AnalogOverlay.NONE -> R.layout.widget_analog_plain
|
||||
@@ -175,9 +197,10 @@ internal fun analogClockViews(context: Context, style: AnalogClockStyle, sideDp:
|
||||
// plain faces are dated.
|
||||
setViewVisibility(R.id.widget_clock_analog_date, View.VISIBLE)
|
||||
setIcon(R.id.widget_clock_analog_date, "setSecondHand", Icon.createWithBitmap(dateHand(context)))
|
||||
setColorStateList(R.id.widget_clock_analog_date, "setSecondHandTintList", R.color.widget_analog_date)
|
||||
setWidgetTint(R.id.widget_clock_analog_date, "setSecondHandTintList", palette.color(WidgetRole.ANALOG_DATE))
|
||||
}
|
||||
}
|
||||
if (!palette.followsLauncher) recolor(context, style, overlay, palette)
|
||||
}
|
||||
if (overlay == AnalogOverlay.NONE) return@apply
|
||||
val inset = frac(0.07f)
|
||||
@@ -209,6 +232,32 @@ internal fun analogClockViews(context: Context, style: AnalogClockStyle, sideDp:
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [style]'s face in [palette]'s colours: the dial, each hand and the
|
||||
* overlay, in the same roles their drawables and layouts name. A hand takes
|
||||
* one tint, so the slim minute hand's two-tone cap comes out in the minute
|
||||
* colour throughout.
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
private fun RemoteViews.recolor(context: Context, style: AnalogClockStyle, overlay: AnalogOverlay, palette: WidgetPalette) {
|
||||
setWidgetColor(context, R.id.widget_analog_dial, "setColorFilter", palette.color(style.dialRole()))
|
||||
setWidgetTint(R.id.widget_clock_analog, "setHourHandTintList", palette.color(WidgetRole.ANALOG_HAND_HOUR))
|
||||
setWidgetTint(R.id.widget_clock_analog, "setMinuteHandTintList", palette.color(WidgetRole.ANALOG_HAND_MINUTE))
|
||||
if (style.seconds() != AnalogSeconds.None) {
|
||||
setWidgetTint(R.id.widget_clock_analog, "setSecondHandTintList", palette.color(WidgetRole.ANALOG_SECOND))
|
||||
}
|
||||
val numerals = when (overlay) {
|
||||
AnalogOverlay.NONE, AnalogOverlay.DATE_PILL -> emptyList()
|
||||
AnalogOverlay.NUMERALS -> listOf(R.id.widget_analog_numeral_12, R.id.widget_analog_numeral_3, R.id.widget_analog_numeral_6, R.id.widget_analog_numeral_9)
|
||||
AnalogOverlay.DAY -> listOf(R.id.widget_analog_numeral_12, R.id.widget_analog_numeral_6, R.id.widget_analog_numeral_9)
|
||||
}
|
||||
numerals.forEach { setWidgetColor(context, it, "setTextColor", palette.color(WidgetRole.ANALOG_NUMERAL)) }
|
||||
if (overlay == AnalogOverlay.DAY || overlay == AnalogOverlay.DATE_PILL) {
|
||||
setWidgetTint(R.id.widget_analog_badge_day, "setBackgroundTintList", palette.color(WidgetRole.ANALOG_BADGE_PILL))
|
||||
setWidgetColor(context, R.id.widget_analog_badge_day, "setTextColor", palette.color(WidgetRole.ANALOG_BADGE_PILL_TEXT))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The date half of Google Clock's seconds hand: today's date ("Fri 2")
|
||||
* opposite the seconds dot, on the layout's separate date clock. Text cannot
|
||||
|
||||
@@ -78,7 +78,8 @@ fun ClockWidgetRoot(entryPoint: WidgetEntryPoint, initialCities: List<ClockWidge
|
||||
* the widget's live size. Like the analog widget, the face draws its own
|
||||
* container (a card, pills, or a shape) and takes its colours from the
|
||||
* `widget_*` palette resources, so it follows the launcher's day/night and
|
||||
* the wallpaper by itself. A tap opens [tapTarget]'s tab.
|
||||
* the wallpaper by itself — unless the instance's [LocalWidgetColors] say
|
||||
* otherwise. A tap opens [tapTarget]'s tab.
|
||||
*/
|
||||
@Composable
|
||||
fun ClockWidgetContent(cities: List<ClockWidgetCity>, settings: DigitalClockSettings, tapTarget: ClockWidgetTapTarget) {
|
||||
@@ -86,7 +87,7 @@ fun ClockWidgetContent(cities: List<ClockWidgetCity>, settings: DigitalClockSett
|
||||
val size = LocalSize.current
|
||||
Box(modifier = GlanceModifier.fillMaxSize().clickable(actionStartActivity(clockTapIntent(context, tapTarget)))) {
|
||||
AndroidRemoteViews(
|
||||
remoteViews = digitalClockViews(context, settings, cities, size.width.value, size.height.value),
|
||||
remoteViews = digitalClockViews(context, settings, cities, size.width.value, size.height.value, LocalWidgetColors.current),
|
||||
modifier = GlanceModifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package de.jeanlucmakiola.clockula.ui.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.text.format.DateFormat
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.widget.RemoteViews
|
||||
import androidx.annotation.RequiresApi
|
||||
import de.jeanlucmakiola.clockula.R
|
||||
import de.jeanlucmakiola.clockula.domain.widget.DigitalClockSettings
|
||||
import de.jeanlucmakiola.clockula.domain.widget.DigitalClockStyle
|
||||
import de.jeanlucmakiola.clockula.domain.widget.WidgetColors
|
||||
import de.jeanlucmakiola.clockula.domain.widget.showsWorldClocks
|
||||
|
||||
/** Below this the time is too small to share the card with world-clock rows, so they drop first. */
|
||||
@@ -27,6 +30,11 @@ private const val MIN_TIME_WITH_DATE = 22f
|
||||
*
|
||||
* The time and date are `TextClock`s ticking in the launcher; the time drops
|
||||
* the 12-hour AM/PM marker, as the platform's own clock widgets do.
|
||||
*
|
||||
* The layouts carry the launcher-resolved palette; [colors] other than the
|
||||
* defaults re-colour them ([recolor]), on API 31+ only — below that a
|
||||
* shape's background tint can't be set through `RemoteViews`, and the config
|
||||
* screen doesn't offer them.
|
||||
*/
|
||||
internal fun digitalClockViews(
|
||||
context: Context,
|
||||
@@ -34,20 +42,67 @@ internal fun digitalClockViews(
|
||||
cities: List<ClockWidgetCity>,
|
||||
widthDp: Float,
|
||||
heightDp: Float,
|
||||
): RemoteViews = when (settings.style) {
|
||||
DigitalClockStyle.REGULAR, DigitalClockStyle.LIGHT, DigitalClockStyle.BOLD ->
|
||||
cardViews(context, settings, cities, widthDp, heightDp)
|
||||
DigitalClockStyle.PILL -> pillViews(context, settings.showDate, widthDp, heightDp)
|
||||
DigitalClockStyle.STACKED -> stackedViews(context, settings.showDate, minOf(widthDp, heightDp))
|
||||
colors: WidgetColors = WidgetColors(),
|
||||
): RemoteViews {
|
||||
val palette = WidgetPalette(context, colors).takeIf { Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !it.followsLauncher }
|
||||
val views = when (settings.style) {
|
||||
DigitalClockStyle.REGULAR, DigitalClockStyle.LIGHT, DigitalClockStyle.BOLD ->
|
||||
cardViews(context, settings, cities, widthDp, heightDp, palette)
|
||||
DigitalClockStyle.PILL -> pillViews(context, settings.showDate, widthDp, heightDp)
|
||||
DigitalClockStyle.STACKED -> stackedViews(context, settings.showDate, minOf(widthDp, heightDp))
|
||||
}
|
||||
if (palette != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) views.recolor(context, settings.style, palette)
|
||||
return views
|
||||
}
|
||||
|
||||
/** The card faces: time, date, then up to three world-clock rows. */
|
||||
/**
|
||||
* [style]'s face in [palette]'s colours: the same roles its layout names
|
||||
* (`res/layout/widget_digital_*.xml`), resolved for this instance. City rows
|
||||
* are re-coloured as [cardViews] adds them.
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
private fun RemoteViews.recolor(context: Context, style: DigitalClockStyle, palette: WidgetPalette) {
|
||||
fun text(viewId: Int, role: WidgetRole) = setWidgetColor(context, viewId, "setTextColor", palette.color(role))
|
||||
fun background(viewId: Int, role: WidgetRole) = setWidgetTint(viewId, "setBackgroundTintList", palette.color(role))
|
||||
when (style) {
|
||||
DigitalClockStyle.REGULAR -> {
|
||||
background(R.id.widget_digital_root, WidgetRole.SURFACE)
|
||||
text(R.id.widget_digital_time, WidgetRole.ON_SURFACE)
|
||||
text(R.id.widget_digital_date, WidgetRole.PRIMARY)
|
||||
}
|
||||
DigitalClockStyle.LIGHT -> {
|
||||
background(R.id.widget_digital_root, WidgetRole.SURFACE)
|
||||
text(R.id.widget_digital_time, WidgetRole.PRIMARY)
|
||||
text(R.id.widget_digital_date, WidgetRole.ON_SURFACE_VARIANT)
|
||||
}
|
||||
DigitalClockStyle.BOLD -> {
|
||||
background(R.id.widget_digital_root, WidgetRole.PRIMARY_CONTAINER)
|
||||
text(R.id.widget_digital_time, WidgetRole.ON_PRIMARY_CONTAINER)
|
||||
text(R.id.widget_digital_date, WidgetRole.ON_PRIMARY_CONTAINER)
|
||||
}
|
||||
DigitalClockStyle.PILL -> {
|
||||
background(R.id.widget_digital_time, WidgetRole.PRIMARY_CONTAINER)
|
||||
text(R.id.widget_digital_time, WidgetRole.ON_PRIMARY_CONTAINER)
|
||||
background(R.id.widget_digital_date, WidgetRole.TERTIARY_CONTAINER)
|
||||
text(R.id.widget_digital_date, WidgetRole.ON_TERTIARY_CONTAINER)
|
||||
}
|
||||
DigitalClockStyle.STACKED -> {
|
||||
setWidgetColor(context, R.id.widget_digital_shape, "setColorFilter", palette.color(WidgetRole.TERTIARY_CONTAINER))
|
||||
text(R.id.widget_digital_hours, WidgetRole.ON_TERTIARY_CONTAINER)
|
||||
text(R.id.widget_digital_minutes, WidgetRole.TERTIARY)
|
||||
text(R.id.widget_digital_date, WidgetRole.ON_TERTIARY_CONTAINER)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The card faces: time, date, then up to three world-clock rows ([palette]'s colours, when re-coloured). */
|
||||
private fun cardViews(
|
||||
context: Context,
|
||||
settings: DigitalClockSettings,
|
||||
cities: List<ClockWidgetCity>,
|
||||
w: Float,
|
||||
h: Float,
|
||||
palette: WidgetPalette?,
|
||||
): RemoteViews {
|
||||
val (layout, cityLayout, emsWide) = when (settings.style) {
|
||||
DigitalClockStyle.LIGHT -> Triple(R.layout.widget_digital_thin, R.layout.widget_digital_city, 2.6f)
|
||||
@@ -91,6 +146,13 @@ private fun cardViews(
|
||||
// Each row after the first sits a little below the one before.
|
||||
if (index > 0) setViewPadding(R.id.widget_digital_city_name, 0, px(small * 0.4f), 0, 0)
|
||||
if (index > 0) setViewPadding(R.id.widget_digital_city_time, 0, px(small * 0.4f), 0, 0)
|
||||
if (palette != null) {
|
||||
val onPrimary = settings.style == DigitalClockStyle.BOLD
|
||||
val name = if (onPrimary) WidgetRole.ON_PRIMARY_CONTAINER else WidgetRole.ON_SURFACE_VARIANT
|
||||
val time = if (onPrimary) WidgetRole.ON_PRIMARY_CONTAINER else WidgetRole.ON_SURFACE
|
||||
setWidgetColor(context, R.id.widget_digital_city_name, "setTextColor", palette.color(name))
|
||||
setWidgetColor(context, R.id.widget_digital_city_time, "setTextColor", palette.color(time))
|
||||
}
|
||||
}
|
||||
addView(R.id.widget_digital_cities, row)
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ private fun AtRest(context: Context, model: QuickTimerWidgetModel, s: Float, sha
|
||||
description = context.getString(R.string.widget_quick_timer_start, label),
|
||||
s = s,
|
||||
shape = shape,
|
||||
) { size -> WidgetReadout(label, size, R.color.widget_on_primary_container) }
|
||||
) { size -> WidgetReadout(label, size, WidgetRole.ON_PRIMARY_CONTAINER) }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,11 +130,12 @@ private fun Running(context: Context, item: TimerWidgetItem, elapsedRealtimeNow:
|
||||
base = elapsedRealtimeNow + item.remaining.inWholeMilliseconds,
|
||||
countDown = true,
|
||||
textSizeDp = size,
|
||||
role = WidgetRole.ON_PRIMARY_CONTAINER,
|
||||
)
|
||||
} else {
|
||||
val text = if (expired) context.getString(R.string.widget_timer_expired) else ClockFormat.countdown(item.remaining)
|
||||
// Shrunk to the same width should the word be longer than a time.
|
||||
WidgetReadout(text, size * minOf(1f, 5f / text.length), R.color.widget_on_primary_container)
|
||||
WidgetReadout(text, size * minOf(1f, 5f / text.length), WidgetRole.ON_PRIMARY_CONTAINER)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,15 +265,16 @@ private fun Readout(context: Context, model: StopwatchWidgetModel, elapsedRealti
|
||||
base = elapsedRealtimeNow - model.elapsed.inWholeMilliseconds,
|
||||
countDown = false,
|
||||
textSizeDp = sizeDp,
|
||||
role = if (onShape) WidgetRole.ON_PRIMARY_CONTAINER else WidgetRole.ON_SURFACE,
|
||||
)
|
||||
} else {
|
||||
WidgetReadout(
|
||||
text = ClockFormat.elapsed(model.elapsed),
|
||||
textSizeDp = sizeDp,
|
||||
colorRes = when {
|
||||
onShape -> R.color.widget_on_primary_container
|
||||
model.mode == StopwatchState.IDLE -> R.color.widget_on_surface_variant
|
||||
else -> R.color.widget_on_surface
|
||||
role = when {
|
||||
onShape -> WidgetRole.ON_PRIMARY_CONTAINER
|
||||
model.mode == StopwatchState.IDLE -> WidgetRole.ON_SURFACE_VARIANT
|
||||
else -> WidgetRole.ON_SURFACE
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -199,12 +199,13 @@ private fun Readout(context: Context, item: TimerWidgetItem, elapsedRealtimeNow:
|
||||
base = elapsedRealtimeNow + item.remaining.inWholeMilliseconds,
|
||||
countDown = true,
|
||||
textSizeDp = timeSize,
|
||||
role = WidgetRole.ON_SURFACE,
|
||||
)
|
||||
} else {
|
||||
WidgetReadout(
|
||||
text = staticReadout(context, item),
|
||||
textSizeDp = timeSize,
|
||||
colorRes = if (item.mode == TimerMode.EXPIRED) R.color.widget_tertiary else R.color.widget_on_surface,
|
||||
role = if (item.mode == TimerMode.EXPIRED) WidgetRole.TERTIARY else WidgetRole.ON_SURFACE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.util.TypedValue
|
||||
import android.widget.RemoteViews
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -35,6 +34,8 @@ import de.jeanlucmakiola.clockula.MainActivity
|
||||
import de.jeanlucmakiola.clockula.R
|
||||
import de.jeanlucmakiola.clockula.domain.widget.ClockWidgetTapTarget
|
||||
import de.jeanlucmakiola.clockula.ui.shell.ClockulaDestination
|
||||
import de.jeanlucmakiola.clockula.domain.widget.WidgetColorTheme
|
||||
import de.jeanlucmakiola.clockula.domain.widget.WidgetColors
|
||||
import de.jeanlucmakiola.clockula.ui.theme.ClockulaDarkFallback
|
||||
import de.jeanlucmakiola.clockula.ui.theme.ClockulaLightFallback
|
||||
import de.jeanlucmakiola.clockula.ui.shell.ShellNavigation
|
||||
@@ -46,7 +47,8 @@ import java.util.Locale
|
||||
* The pieces every widget but the analog one is built from — the same
|
||||
* patterns `AnalogClockWidgetContent.kt` set:
|
||||
* - colour follows the launcher's day/night and, on API 31+, the wallpaper
|
||||
* by itself ([WidgetTheme]) — no app-side theme pinning;
|
||||
* by itself ([WidgetTheme]) — unless the instance's own [WidgetColors]
|
||||
* pin a tone or switch the wallpaper's colours off (`WidgetPalette.kt`);
|
||||
* - containers are M3 shapes (`widget_bg_*`, `widget_shape_*`), white
|
||||
* drawables tinted here;
|
||||
* - every size is a fraction of the widget's live `LocalSize` (`SizeMode.Exact`
|
||||
@@ -55,18 +57,25 @@ import java.util.Locale
|
||||
*/
|
||||
|
||||
/**
|
||||
* The widgets' colours: on API 31+ Glance's own dynamic scheme — resource
|
||||
* colours the launcher resolves against its day/night and the wallpaper, the
|
||||
* same system tones as the `widget_*` palette the plain-`RemoteViews` faces
|
||||
* use — and the app's own fallback schemes below that.
|
||||
* The widgets' colours, per the instance's [LocalWidgetColors]. By default,
|
||||
* on API 31+ Glance's own dynamic scheme — resource colours the launcher
|
||||
* resolves against its day/night and the wallpaper, the same system tones as
|
||||
* the `widget_*` palette the plain-`RemoteViews` faces use — and the app's
|
||||
* own fallback schemes below that, or always with wallpaper colours off.
|
||||
* Light or Dark pins that one scheme whatever the launcher's day/night.
|
||||
*/
|
||||
@Composable
|
||||
internal fun WidgetTheme(content: @Composable () -> Unit) {
|
||||
val colors = LocalWidgetColors.current
|
||||
FloretGlanceTheme(
|
||||
lightScheme = ClockulaLightFallback,
|
||||
darkScheme = ClockulaDarkFallback,
|
||||
dynamicColor = true,
|
||||
mode = WidgetThemeMode.SYSTEM,
|
||||
dynamicColor = colors.wallpaperColors,
|
||||
mode = when (colors.theme) {
|
||||
WidgetColorTheme.SYSTEM -> WidgetThemeMode.SYSTEM
|
||||
WidgetColorTheme.LIGHT -> WidgetThemeMode.LIGHT
|
||||
WidgetColorTheme.DARK -> WidgetThemeMode.DARK
|
||||
},
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
@@ -128,16 +137,20 @@ internal fun ShapedIcon(
|
||||
|
||||
/**
|
||||
* A platform `Chronometer` ([layoutRes], ticking in the launcher, not the
|
||||
* app) at a proportional [textSizeDp]. [base] is in
|
||||
* `SystemClock.elapsedRealtime()` time, as `RemoteViews.setChronometer` wants.
|
||||
* app) at a proportional [textSizeDp], in [role] — the colour [layoutRes]
|
||||
* already sets, which only needs repeating when the instance's colours
|
||||
* aren't the launcher's. [base] is in `SystemClock.elapsedRealtime()` time,
|
||||
* as `RemoteViews.setChronometer` wants.
|
||||
*/
|
||||
@Composable
|
||||
internal fun WidgetChronometer(@LayoutRes layoutRes: Int, viewId: Int, base: Long, countDown: Boolean, textSizeDp: Float) {
|
||||
internal fun WidgetChronometer(@LayoutRes layoutRes: Int, viewId: Int, base: Long, countDown: Boolean, textSizeDp: Float, role: WidgetRole) {
|
||||
val context = LocalContext.current
|
||||
val palette = WidgetPalette(context, LocalWidgetColors.current)
|
||||
val views = RemoteViews(context.packageName, layoutRes).apply {
|
||||
setChronometer(viewId, base, null, true)
|
||||
setChronometerCountDown(viewId, countDown)
|
||||
setTextViewTextSize(viewId, TypedValue.COMPLEX_UNIT_DIP, textSizeDp)
|
||||
if (!palette.followsLauncher) setWidgetColor(context, viewId, "setTextColor", palette.color(role))
|
||||
}
|
||||
// Without an explicit wrap, Glance lets embedded views fill their
|
||||
// parent, and the readout pushes everything after it out of the widget.
|
||||
@@ -145,23 +158,20 @@ internal fun WidgetChronometer(@LayoutRes layoutRes: Int, viewId: Int, base: Lon
|
||||
}
|
||||
|
||||
/**
|
||||
* A stopped readout ([text] at [textSizeDp], in palette colour [colorRes]):
|
||||
* A stopped readout ([text] at [textSizeDp], in palette colour [role]):
|
||||
* the same padding-free view as [WidgetChronometer]'s, unlike a Glance
|
||||
* `Text`, so a readout takes the same height whether it is ticking or not.
|
||||
*/
|
||||
@Composable
|
||||
internal fun WidgetReadout(text: String, textSizeDp: Float, @ColorRes colorRes: Int) {
|
||||
internal fun WidgetReadout(text: String, textSizeDp: Float, role: WidgetRole) {
|
||||
val context = LocalContext.current
|
||||
val palette = WidgetPalette(context, LocalWidgetColors.current)
|
||||
val views = RemoteViews(context.packageName, R.layout.widget_readout).apply {
|
||||
setTextViewText(R.id.widget_readout, text)
|
||||
setTextViewTextSize(R.id.widget_readout, TypedValue.COMPLEX_UNIT_DIP, textSizeDp)
|
||||
// From API 31 the launcher resolves the colour against its own
|
||||
// day/night; before that it can only be resolved here.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
setColorStateList(R.id.widget_readout, "setTextColor", colorRes)
|
||||
} else {
|
||||
setTextColor(R.id.widget_readout, context.getColor(colorRes))
|
||||
}
|
||||
// By default, from API 31 the launcher resolves the colour against
|
||||
// its own day/night; before that it can only be resolved here.
|
||||
setWidgetColor(context, R.id.widget_readout, "setTextColor", palette.color(role))
|
||||
}
|
||||
AndroidRemoteViews(remoteViews = views, modifier = GlanceModifier.wrapContentSize())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
package de.jeanlucmakiola.clockula.ui.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.widget.RemoteViews
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.glance.currentState
|
||||
import de.jeanlucmakiola.clockula.R
|
||||
import de.jeanlucmakiola.clockula.domain.widget.WidgetColors
|
||||
import de.jeanlucmakiola.clockula.domain.widget.WidgetPaletteMode
|
||||
import de.jeanlucmakiola.clockula.domain.widget.paletteMode
|
||||
import de.jeanlucmakiola.clockula.widget.widgetColors
|
||||
|
||||
/*
|
||||
* A widget instance's own colours ([WidgetColors]) for the parts Glance's
|
||||
* theme can't reach: the plain-`RemoteViews` faces, readouts and
|
||||
* chronometers, whose colours are the `widget_*` palette resources. By
|
||||
* default the launcher resolves those itself, against its day/night and
|
||||
* wallpaper; anything else is resolved here, from the same resources under a
|
||||
* forced day or night configuration, and handed to the views as concrete
|
||||
* colours (or, on API 31+, a day/night pair the launcher picks between).
|
||||
*/
|
||||
|
||||
/** The current widget instance's [WidgetColors]; the defaults outside a widget session. */
|
||||
internal val LocalWidgetColors = staticCompositionLocalOf { WidgetColors() }
|
||||
|
||||
/** Provides this widget instance's stored [WidgetColors] as [LocalWidgetColors]. Inside `provideContent` only. */
|
||||
@Composable
|
||||
internal fun ProvideWidgetColors(content: @Composable () -> Unit) {
|
||||
CompositionLocalProvider(LocalWidgetColors provides currentState<Preferences>().widgetColors(), content = content)
|
||||
}
|
||||
|
||||
/**
|
||||
* One `widget_*` palette role: [launcher] is the resource the widgets
|
||||
* reference (the wallpaper's tones on API 31+), [app] its app-palette tone,
|
||||
* which stays reachable on every API level (`res/values/colors.xml`).
|
||||
*/
|
||||
internal enum class WidgetRole(@ColorRes val launcher: Int, @ColorRes val app: Int) {
|
||||
SURFACE(R.color.widget_surface, R.color.widget_app_surface),
|
||||
ON_SURFACE(R.color.widget_on_surface, R.color.widget_app_on_surface),
|
||||
ON_SURFACE_VARIANT(R.color.widget_on_surface_variant, R.color.widget_app_on_surface_variant),
|
||||
PRIMARY(R.color.widget_primary, R.color.widget_app_primary),
|
||||
PRIMARY_CONTAINER(R.color.widget_primary_container, R.color.widget_app_primary_container),
|
||||
ON_PRIMARY_CONTAINER(R.color.widget_on_primary_container, R.color.widget_app_on_primary_container),
|
||||
TERTIARY(R.color.widget_tertiary, R.color.widget_app_tertiary),
|
||||
TERTIARY_CONTAINER(R.color.widget_tertiary_container, R.color.widget_app_tertiary_container),
|
||||
ON_TERTIARY_CONTAINER(R.color.widget_on_tertiary_container, R.color.widget_app_on_tertiary_container),
|
||||
ANALOG_DIAL_FILL(R.color.widget_analog_dial_fill, R.color.widget_app_analog_dial_fill),
|
||||
ANALOG_DIAL_PRIMARY(R.color.widget_analog_dial_primary, R.color.widget_app_analog_dial_primary),
|
||||
ANALOG_DIAL_TERTIARY(R.color.widget_analog_dial_tertiary, R.color.widget_app_analog_dial_tertiary),
|
||||
ANALOG_HAND_HOUR(R.color.widget_analog_hand_hour, R.color.widget_app_analog_hand_hour),
|
||||
ANALOG_HAND_MINUTE(R.color.widget_analog_hand_minute, R.color.widget_app_analog_hand_minute),
|
||||
ANALOG_NUMERAL(R.color.widget_analog_numeral, R.color.widget_app_analog_numeral),
|
||||
ANALOG_SECOND(R.color.widget_analog_second, R.color.widget_app_analog_second),
|
||||
ANALOG_DATE(R.color.widget_analog_date, R.color.widget_app_analog_date),
|
||||
ANALOG_BADGE_PILL(R.color.widget_analog_badge_pill, R.color.widget_app_analog_badge_pill),
|
||||
ANALOG_BADGE_PILL_TEXT(R.color.widget_analog_badge_pill_text, R.color.widget_app_analog_badge_pill_text),
|
||||
}
|
||||
|
||||
/** One role's colour as it reaches a view: a resource the launcher resolves, one fixed colour, or a day/night pair. */
|
||||
internal sealed interface WidgetColor {
|
||||
data class Launcher(@ColorRes val res: Int) : WidgetColor
|
||||
data class Fixed(@ColorInt val color: Int) : WidgetColor
|
||||
data class DayNight(@ColorInt val day: Int, @ColorInt val night: Int) : WidgetColor
|
||||
}
|
||||
|
||||
/** [colors] resolved for this device, one [WidgetRole] at a time. */
|
||||
internal class WidgetPalette(private val context: Context, colors: WidgetColors) {
|
||||
|
||||
val mode: WidgetPaletteMode = colors.paletteMode(dynamicAvailable = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
|
||||
/** True for the default look, where the layouts' own resource colours are already right. */
|
||||
val followsLauncher: Boolean get() = mode == WidgetPaletteMode.Launcher
|
||||
|
||||
private val day by lazy { tone(dark = false) }
|
||||
private val night by lazy { tone(dark = true) }
|
||||
|
||||
fun color(role: WidgetRole): WidgetColor = when (val mode = mode) {
|
||||
WidgetPaletteMode.Launcher -> WidgetColor.Launcher(role.launcher)
|
||||
WidgetPaletteMode.AppDayNight -> WidgetColor.DayNight(day.getColor(role.app), night.getColor(role.app))
|
||||
is WidgetPaletteMode.Pinned ->
|
||||
WidgetColor.Fixed((if (mode.dark) night else day).getColor(if (mode.dynamic) role.launcher else role.app))
|
||||
}
|
||||
|
||||
/**
|
||||
* The app's resources under a forced day or night: `values-night` (and
|
||||
* `-night-v31`'s wallpaper tones) by choice rather than by whatever the
|
||||
* app's own configuration happens to be.
|
||||
*/
|
||||
private fun tone(dark: Boolean): Context {
|
||||
val config = Configuration(context.resources.configuration)
|
||||
val night = if (dark) Configuration.UI_MODE_NIGHT_YES else Configuration.UI_MODE_NIGHT_NO
|
||||
config.uiMode = (config.uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or night
|
||||
return context.createConfigurationContext(config)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [color] through [method], a view method taking an `int` colour
|
||||
* (`setTextColor`, `setColorFilter`). Every API level; [WidgetColor.DayNight]
|
||||
* only ever arises on API 31+.
|
||||
*/
|
||||
internal fun RemoteViews.setWidgetColor(context: Context, viewId: Int, method: String, color: WidgetColor) {
|
||||
when (color) {
|
||||
is WidgetColor.Launcher -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
// Resolved by the launcher, against its own day/night.
|
||||
setColor(viewId, method, color.res)
|
||||
} else {
|
||||
setInt(viewId, method, context.getColor(color.res))
|
||||
}
|
||||
is WidgetColor.Fixed -> setInt(viewId, method, color.color)
|
||||
is WidgetColor.DayNight -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
setColorInt(viewId, method, color.day, color.night)
|
||||
} else {
|
||||
setInt(viewId, method, color.day)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** [color] through [method], a view method taking a `ColorStateList` (`setBackgroundTintList`, an `AnalogClock` hand's tint). */
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
internal fun RemoteViews.setWidgetTint(viewId: Int, method: String, color: WidgetColor) {
|
||||
when (color) {
|
||||
is WidgetColor.Launcher -> setColorStateList(viewId, method, color.res)
|
||||
is WidgetColor.Fixed -> setColorStateList(viewId, method, ColorStateList.valueOf(color.color))
|
||||
is WidgetColor.DayNight ->
|
||||
setColorStateList(viewId, method, ColorStateList.valueOf(color.day), ColorStateList.valueOf(color.night))
|
||||
}
|
||||
}
|
||||
+23
-23
@@ -1,21 +1,15 @@
|
||||
package de.jeanlucmakiola.clockula.ui.widget.config
|
||||
|
||||
import android.widget.FrameLayout
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
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.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -28,21 +22,24 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import de.jeanlucmakiola.clockula.R
|
||||
import de.jeanlucmakiola.clockula.domain.widget.AnalogClockStyle
|
||||
import de.jeanlucmakiola.clockula.domain.widget.ClockWidgetTapTarget
|
||||
import de.jeanlucmakiola.clockula.domain.widget.WidgetColors
|
||||
import de.jeanlucmakiola.clockula.domain.widget.parseAnalogClockStyle
|
||||
import de.jeanlucmakiola.clockula.domain.widget.parseClockWidgetTapTarget
|
||||
import de.jeanlucmakiola.clockula.ui.widget.analogClockViews
|
||||
import de.jeanlucmakiola.clockula.widget.AnalogClockWidget
|
||||
import de.jeanlucmakiola.clockula.widget.WIDGET_ANALOG_STYLE_KEY
|
||||
import de.jeanlucmakiola.clockula.widget.WIDGET_TAP_TARGET_KEY
|
||||
import de.jeanlucmakiola.floret.components.FullScreenPicker
|
||||
import de.jeanlucmakiola.clockula.widget.setWidgetColors
|
||||
import de.jeanlucmakiola.clockula.widget.widgetColors
|
||||
import de.jeanlucmakiola.floret.components.GroupedRow
|
||||
import de.jeanlucmakiola.floret.components.Position
|
||||
|
||||
/**
|
||||
* The analog widget's configuration: its style and the tab a tap opens (#8).
|
||||
* Laid out like the digital widget's — the live face above the settings, the
|
||||
* style behind a row that opens [WidgetStylePicker], saved with OK — since a
|
||||
* second setting means a tap on a style can no longer be the whole answer.
|
||||
* The analog widget's configuration: its style, the tab a tap opens (#8) and,
|
||||
* on API 31+, its colours ([WidgetColorsSection]). Laid out like the digital
|
||||
* widget's — the live face above the settings, the style behind a row that
|
||||
* opens [WidgetStylePicker], saved with OK — since more than one setting
|
||||
* means a tap on a style can no longer be the whole answer.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class AnalogClockConfigActivity : WidgetConfigActivity() {
|
||||
@@ -54,22 +51,28 @@ class AnalogClockConfigActivity : WidgetConfigActivity() {
|
||||
// defaults over what the user has.
|
||||
var style by remember { mutableStateOf<AnalogClockStyle?>(null) }
|
||||
var tapTarget by remember { mutableStateOf(ClockWidgetTapTarget.ALARMS) }
|
||||
var colors by remember { mutableStateOf<WidgetColors?>(null) }
|
||||
LaunchedEffect(glanceId) {
|
||||
val state = getAppWidgetState(this@AnalogClockConfigActivity, PreferencesGlanceStateDefinition, glanceId)
|
||||
tapTarget = parseClockWidgetTapTarget(state[WIDGET_TAP_TARGET_KEY])
|
||||
style = parseAnalogClockStyle(state[WIDGET_ANALOG_STYLE_KEY])
|
||||
colors = state.widgetColors()
|
||||
}
|
||||
val current = style ?: return
|
||||
val currentColors = colors ?: return
|
||||
AnalogClockConfigScreen(
|
||||
style = current,
|
||||
onStyleChange = { style = it },
|
||||
tapTarget = tapTarget,
|
||||
onTapTargetChange = { tapTarget = it },
|
||||
colors = currentColors,
|
||||
onColorsChange = { colors = it },
|
||||
onSave = {
|
||||
save {
|
||||
updateAppWidgetState(this@AnalogClockConfigActivity, glanceId) { prefs ->
|
||||
prefs[WIDGET_ANALOG_STYLE_KEY] = current.name
|
||||
prefs[WIDGET_TAP_TARGET_KEY] = tapTarget.name
|
||||
prefs.setWidgetColors(currentColors)
|
||||
}
|
||||
AnalogClockWidget().update(this@AnalogClockConfigActivity, glanceId)
|
||||
}
|
||||
@@ -85,22 +88,19 @@ private fun AnalogClockConfigScreen(
|
||||
onStyleChange: (AnalogClockStyle) -> Unit,
|
||||
tapTarget: ClockWidgetTapTarget,
|
||||
onTapTargetChange: (ClockWidgetTapTarget) -> Unit,
|
||||
colors: WidgetColors,
|
||||
onColorsChange: (WidgetColors) -> Unit,
|
||||
onSave: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
var showStylePicker by remember { mutableStateOf(false) }
|
||||
|
||||
FullScreenPicker(
|
||||
WidgetSettingsScreen(
|
||||
title = stringResource(R.string.widget_analog_config_title),
|
||||
onSave = onSave,
|
||||
onDismiss = onDismiss,
|
||||
actions = {
|
||||
TextButton(onClick = onSave) { Text(stringResource(android.R.string.ok)) }
|
||||
},
|
||||
preview = { AnalogStylePreview(style, colors) },
|
||||
) {
|
||||
// The widget as it will look, live, above the settings that change it.
|
||||
Box(modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), contentAlignment = Alignment.Center) {
|
||||
AnalogStylePreview(style)
|
||||
}
|
||||
GroupedRow(
|
||||
title = stringResource(R.string.widget_config_style),
|
||||
summary = stringResource(analogStyleLabel(style)),
|
||||
@@ -109,7 +109,7 @@ private fun AnalogClockConfigScreen(
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
TapTargetRow(target = tapTarget, position = Position.Alone, onTargetChange = onTapTargetChange)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
WidgetColorsSection(colors = colors, onColorsChange = onColorsChange, available = WidgetDynamicColorAvailable)
|
||||
}
|
||||
|
||||
if (showStylePicker) {
|
||||
@@ -118,7 +118,7 @@ private fun AnalogClockConfigScreen(
|
||||
options = AnalogClockStyle.entries,
|
||||
selected = style,
|
||||
label = { stringResource(analogStyleLabel(it)) },
|
||||
preview = { AnalogStylePreview(it) },
|
||||
preview = { AnalogStylePreview(it, colors) },
|
||||
onSelect = onStyleChange,
|
||||
onDismiss = { showStylePicker = false },
|
||||
)
|
||||
@@ -143,9 +143,9 @@ internal fun analogStyleLabel(style: AnalogClockStyle): Int = when (style) {
|
||||
* ticks and the dial, numerals and colours match exactly.
|
||||
*/
|
||||
@Composable
|
||||
private fun AnalogStylePreview(style: AnalogClockStyle) {
|
||||
private fun AnalogStylePreview(style: AnalogClockStyle, colors: WidgetColors) {
|
||||
val context = LocalContext.current
|
||||
val views = remember(style) { analogClockViews(context, style, PreviewSize.value) }
|
||||
val views = remember(style, colors) { analogClockViews(context, style, PreviewSize.value, colors) }
|
||||
AndroidView(
|
||||
factory = { FrameLayout(it) },
|
||||
update = { frame ->
|
||||
|
||||
+18
-6
@@ -34,6 +34,7 @@ import de.jeanlucmakiola.clockula.domain.widget.ClockWidgetTapTarget
|
||||
import de.jeanlucmakiola.clockula.domain.widget.DigitalClockSettings
|
||||
import de.jeanlucmakiola.clockula.domain.widget.DigitalClockStyle
|
||||
import de.jeanlucmakiola.clockula.domain.widget.parseClockWidgetTapTarget
|
||||
import de.jeanlucmakiola.clockula.domain.widget.WidgetColors
|
||||
import de.jeanlucmakiola.clockula.domain.widget.showsWorldClocks
|
||||
import de.jeanlucmakiola.clockula.ui.widget.ClockWidgetCity
|
||||
import de.jeanlucmakiola.clockula.ui.widget.digitalClockViews
|
||||
@@ -44,12 +45,14 @@ import de.jeanlucmakiola.clockula.widget.WIDGET_SHOW_DATE_KEY
|
||||
import de.jeanlucmakiola.clockula.widget.WIDGET_SHOW_WORLD_CLOCKS_KEY
|
||||
import de.jeanlucmakiola.clockula.widget.WIDGET_TAP_TARGET_KEY
|
||||
import de.jeanlucmakiola.clockula.widget.parseDigitalClockSettings
|
||||
import de.jeanlucmakiola.clockula.widget.setWidgetColors
|
||||
import de.jeanlucmakiola.clockula.widget.widgetColors
|
||||
import de.jeanlucmakiola.floret.components.FullScreenPicker
|
||||
import de.jeanlucmakiola.floret.components.GroupedRow
|
||||
import de.jeanlucmakiola.floret.components.Position
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Picks the digital widget instance's style, the two switches, and the tab a tap opens. */
|
||||
/** Picks the digital widget instance's style, the two switches, the tab a tap opens, and its colours. */
|
||||
@AndroidEntryPoint
|
||||
class ClockWidgetConfigActivity : WidgetConfigActivity() {
|
||||
|
||||
@@ -63,6 +66,7 @@ class ClockWidgetConfigActivity : WidgetConfigActivity() {
|
||||
// late load can write defaults over what the user has.
|
||||
var settings by remember { mutableStateOf<DigitalClockSettings?>(null) }
|
||||
var tapTarget by remember { mutableStateOf(ClockWidgetTapTarget.ALARMS) }
|
||||
var colors by remember { mutableStateOf<WidgetColors?>(null) }
|
||||
val cities by produceState(initialValue = emptyList<ClockWidgetCity>()) {
|
||||
value = widgetCities(worldClockRepository, zoneNames)
|
||||
}
|
||||
@@ -74,14 +78,18 @@ class ClockWidgetConfigActivity : WidgetConfigActivity() {
|
||||
showDate = state[WIDGET_SHOW_DATE_KEY],
|
||||
showWorldClocks = state[WIDGET_SHOW_WORLD_CLOCKS_KEY],
|
||||
)
|
||||
colors = state.widgetColors()
|
||||
}
|
||||
val current = settings ?: return
|
||||
val currentColors = colors ?: return
|
||||
ClockWidgetConfigScreen(
|
||||
settings = current,
|
||||
cities = cities,
|
||||
onSettingsChange = { settings = it },
|
||||
tapTarget = tapTarget,
|
||||
onTapTargetChange = { tapTarget = it },
|
||||
colors = currentColors,
|
||||
onColorsChange = { colors = it },
|
||||
onSave = {
|
||||
save {
|
||||
updateAppWidgetState(this@ClockWidgetConfigActivity, glanceId) { prefs ->
|
||||
@@ -89,6 +97,7 @@ class ClockWidgetConfigActivity : WidgetConfigActivity() {
|
||||
prefs[WIDGET_SHOW_DATE_KEY] = current.showDate
|
||||
prefs[WIDGET_SHOW_WORLD_CLOCKS_KEY] = current.showWorldClocks
|
||||
prefs[WIDGET_TAP_TARGET_KEY] = tapTarget.name
|
||||
prefs.setWidgetColors(currentColors)
|
||||
}
|
||||
ClockWidget().update(this@ClockWidgetConfigActivity, glanceId)
|
||||
}
|
||||
@@ -105,6 +114,8 @@ private fun ClockWidgetConfigScreen(
|
||||
onSettingsChange: (DigitalClockSettings) -> Unit,
|
||||
tapTarget: ClockWidgetTapTarget,
|
||||
onTapTargetChange: (ClockWidgetTapTarget) -> Unit,
|
||||
colors: WidgetColors,
|
||||
onColorsChange: (WidgetColors) -> Unit,
|
||||
onSave: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
@@ -120,7 +131,7 @@ private fun ClockWidgetConfigScreen(
|
||||
) {
|
||||
// The widget as it will look, live, above the settings that change it.
|
||||
Box(modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), contentAlignment = Alignment.Center) {
|
||||
DigitalStylePreview(settings, cities)
|
||||
DigitalStylePreview(settings, cities, colors)
|
||||
}
|
||||
GroupedRow(
|
||||
title = stringResource(R.string.widget_config_style),
|
||||
@@ -146,6 +157,7 @@ private fun ClockWidgetConfigScreen(
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
TapTargetRow(target = tapTarget, position = Position.Alone, onTargetChange = onTapTargetChange)
|
||||
WidgetColorsSection(colors = colors, onColorsChange = onColorsChange, available = WidgetDynamicColorAvailable)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
}
|
||||
|
||||
@@ -155,7 +167,7 @@ private fun ClockWidgetConfigScreen(
|
||||
options = DigitalClockStyle.entries,
|
||||
selected = settings.style,
|
||||
label = { stringResource(digitalStyleLabel(it)) },
|
||||
preview = { style -> DigitalStylePreview(settings.copy(style = style), cities) },
|
||||
preview = { style -> DigitalStylePreview(settings.copy(style = style), cities, colors) },
|
||||
onSelect = { onSettingsChange(settings.copy(style = it)) },
|
||||
onDismiss = { showStylePicker = false },
|
||||
)
|
||||
@@ -176,10 +188,10 @@ internal fun digitalStyleLabel(style: DigitalClockStyle): Int = when (style) {
|
||||
* tick and the layout, sizes and colours match exactly.
|
||||
*/
|
||||
@Composable
|
||||
private fun DigitalStylePreview(settings: DigitalClockSettings, cities: List<ClockWidgetCity>) {
|
||||
private fun DigitalStylePreview(settings: DigitalClockSettings, cities: List<ClockWidgetCity>, colors: WidgetColors) {
|
||||
val context = LocalContext.current
|
||||
val views = remember(settings, cities) {
|
||||
digitalClockViews(context, settings, cities, PreviewWidth.value, PreviewHeight.value)
|
||||
val views = remember(settings, cities, colors) {
|
||||
digitalClockViews(context, settings, cities, PreviewWidth.value, PreviewHeight.value, colors)
|
||||
}
|
||||
AndroidView(
|
||||
factory = { FrameLayout(it) },
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package de.jeanlucmakiola.clockula.ui.widget.config
|
||||
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import de.jeanlucmakiola.clockula.R
|
||||
import de.jeanlucmakiola.clockula.widget.NextAlarmWidget
|
||||
|
||||
/** The next-alarm widget's colours. */
|
||||
@AndroidEntryPoint
|
||||
class NextAlarmConfigActivity : WidgetColorsConfigActivity() {
|
||||
override val titleRes: Int = R.string.widget_next_alarm_label
|
||||
override fun widget(): GlanceAppWidget = NextAlarmWidget()
|
||||
}
|
||||
+45
-14
@@ -15,46 +15,72 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import de.jeanlucmakiola.clockula.R
|
||||
import de.jeanlucmakiola.clockula.domain.format.ClockFormat
|
||||
import de.jeanlucmakiola.clockula.domain.widget.QuickTimerWidgetSettings
|
||||
import de.jeanlucmakiola.clockula.domain.widget.WidgetColors
|
||||
import de.jeanlucmakiola.clockula.domain.widget.parseQuickTimerDuration
|
||||
import de.jeanlucmakiola.clockula.ui.timers.TimerDurationDialog
|
||||
import de.jeanlucmakiola.clockula.widget.QuickTimerWidget
|
||||
import de.jeanlucmakiola.clockula.widget.WIDGET_QUICK_TIMER_DURATION_KEY
|
||||
import de.jeanlucmakiola.clockula.widget.setWidgetColors
|
||||
import de.jeanlucmakiola.clockula.widget.widgetColors
|
||||
import de.jeanlucmakiola.floret.components.FullScreenPicker
|
||||
import de.jeanlucmakiola.floret.components.GroupedRow
|
||||
import de.jeanlucmakiola.floret.components.Position
|
||||
import de.jeanlucmakiola.floret.components.SelectedCheck
|
||||
import de.jeanlucmakiola.floret.components.positionOf
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
* The quick-timer widget's entire configuration: which duration it starts on
|
||||
* tap — one of [QuickTimerWidgetSettings.OPTIONS], the Timers screen's own
|
||||
* built-in quick-start durations up to the widget's 59:59 cap, or a custom
|
||||
* one typed on the timer keypad. Like [AnalogClockConfigActivity], picking a
|
||||
* duration saves and finishes immediately.
|
||||
* The quick-timer widget's configuration: which duration it starts on tap —
|
||||
* one of [QuickTimerWidgetSettings.OPTIONS], the Timers screen's own built-in
|
||||
* quick-start durations up to the widget's 59:59 cap, or a custom one typed
|
||||
* on the timer keypad — and its colours ([WidgetColorsSection]), saved
|
||||
* together on OK.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class QuickTimerConfigActivity : WidgetConfigActivity() {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
var duration by remember { mutableStateOf(QuickTimerWidgetSettings.DEFAULT) }
|
||||
// `null` until read: see `ClockWidgetConfigActivity`.
|
||||
var duration by remember { mutableStateOf<Duration?>(null) }
|
||||
var colors by remember { mutableStateOf<WidgetColors?>(null) }
|
||||
var showDurationPicker by rememberSaveable { mutableStateOf(false) }
|
||||
LaunchedEffect(glanceId) {
|
||||
val state = getAppWidgetState(this@QuickTimerConfigActivity, PreferencesGlanceStateDefinition, glanceId)
|
||||
duration = parseQuickTimerDuration(state[WIDGET_QUICK_TIMER_DURATION_KEY])
|
||||
colors = state.widgetColors()
|
||||
}
|
||||
QuickTimerPicker(
|
||||
selected = duration,
|
||||
onSelect = { picked ->
|
||||
duration = picked
|
||||
val currentDuration = duration ?: return
|
||||
val currentColors = colors ?: return
|
||||
WidgetSettingsScreen(
|
||||
title = stringResource(R.string.widget_quick_timer_config_title),
|
||||
onSave = {
|
||||
save {
|
||||
updateAppWidgetState(this@QuickTimerConfigActivity, glanceId) { prefs ->
|
||||
prefs[WIDGET_QUICK_TIMER_DURATION_KEY] = picked.inWholeMilliseconds
|
||||
prefs[WIDGET_QUICK_TIMER_DURATION_KEY] = currentDuration.inWholeMilliseconds
|
||||
prefs.setWidgetColors(currentColors)
|
||||
}
|
||||
QuickTimerWidget().update(this@QuickTimerConfigActivity, glanceId)
|
||||
}
|
||||
},
|
||||
onDismiss = ::dismiss,
|
||||
)
|
||||
) {
|
||||
GroupedRow(
|
||||
title = stringResource(R.string.timer_editor_duration),
|
||||
summary = ClockFormat.countdown(currentDuration),
|
||||
position = Position.Alone,
|
||||
onClick = { showDurationPicker = true },
|
||||
)
|
||||
WidgetColorsSection(colors = currentColors, onColorsChange = { colors = it })
|
||||
}
|
||||
|
||||
if (showDurationPicker) {
|
||||
QuickTimerPicker(
|
||||
selected = currentDuration,
|
||||
onSelect = { duration = it },
|
||||
onDismiss = { showDurationPicker = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +88,7 @@ class QuickTimerConfigActivity : WidgetConfigActivity() {
|
||||
* The listed durations, then "Custom…", which opens the timer keypad. A
|
||||
* stored duration that isn't listed marks the Custom row instead, with the
|
||||
* value as its summary, so the picker always shows what the widget starts.
|
||||
* Picking either closes it.
|
||||
*/
|
||||
@Composable
|
||||
private fun QuickTimerPicker(selected: Duration, onSelect: (Duration) -> Unit, onDismiss: () -> Unit) {
|
||||
@@ -69,7 +96,7 @@ private fun QuickTimerPicker(selected: Duration, onSelect: (Duration) -> Unit, o
|
||||
val options = QuickTimerWidgetSettings.OPTIONS
|
||||
val count = options.size + 1
|
||||
|
||||
FullScreenPicker(title = stringResource(R.string.widget_quick_timer_config_title), onDismiss = onDismiss) {
|
||||
FullScreenPicker(title = stringResource(R.string.timer_editor_duration), onDismiss = onDismiss) {
|
||||
options.forEachIndexed { index, option ->
|
||||
val isSelected = option == selected
|
||||
GroupedRow(
|
||||
@@ -81,7 +108,10 @@ private fun QuickTimerPicker(selected: Duration, onSelect: (Duration) -> Unit, o
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onClick = { onSelect(option) },
|
||||
onClick = {
|
||||
onSelect(option)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
val custom = QuickTimerWidgetSettings.isCustom(selected)
|
||||
@@ -107,6 +137,7 @@ private fun QuickTimerPicker(selected: Duration, onSelect: (Duration) -> Unit, o
|
||||
onConfirm = {
|
||||
typing = false
|
||||
onSelect(it)
|
||||
onDismiss()
|
||||
},
|
||||
onDismiss = { typing = false },
|
||||
)
|
||||
|
||||
+68
-15
@@ -6,6 +6,7 @@ import android.widget.FrameLayout
|
||||
import android.widget.RemoteViews
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -28,37 +29,48 @@ import de.jeanlucmakiola.clockula.R
|
||||
import de.jeanlucmakiola.clockula.domain.StopwatchState
|
||||
import de.jeanlucmakiola.clockula.domain.widget.StopwatchWidgetModel
|
||||
import de.jeanlucmakiola.clockula.domain.widget.StopwatchWidgetStyle
|
||||
import de.jeanlucmakiola.clockula.domain.widget.WidgetColors
|
||||
import de.jeanlucmakiola.clockula.domain.widget.parseStopwatchWidgetStyle
|
||||
import de.jeanlucmakiola.clockula.ui.widget.LocalWidgetColors
|
||||
import de.jeanlucmakiola.clockula.ui.widget.StopwatchWidgetContent
|
||||
import de.jeanlucmakiola.clockula.widget.StopwatchWidget
|
||||
import de.jeanlucmakiola.clockula.widget.WIDGET_STOPWATCH_STYLE_KEY
|
||||
import de.jeanlucmakiola.clockula.widget.setWidgetColors
|
||||
import de.jeanlucmakiola.clockula.widget.widgetColors
|
||||
import de.jeanlucmakiola.floret.components.GroupedRow
|
||||
import de.jeanlucmakiola.floret.components.Position
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* The stopwatch widget's entire configuration: a style pick. Tapping an
|
||||
* option saves and finishes immediately.
|
||||
* The stopwatch widget's configuration: its style and its colours
|
||||
* ([WidgetColorsSection]), under a live preview, saved together on OK — the
|
||||
* shape of [ClockWidgetConfigActivity].
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class StopwatchConfigActivity : WidgetConfigActivity() {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
var style by remember { mutableStateOf(StopwatchWidgetStyle.CARD) }
|
||||
// `null` until read: see `ClockWidgetConfigActivity`.
|
||||
var style by remember { mutableStateOf<StopwatchWidgetStyle?>(null) }
|
||||
var colors by remember { mutableStateOf<WidgetColors?>(null) }
|
||||
LaunchedEffect(glanceId) {
|
||||
val state = getAppWidgetState(this@StopwatchConfigActivity, PreferencesGlanceStateDefinition, glanceId)
|
||||
style = parseStopwatchWidgetStyle(state[WIDGET_STOPWATCH_STYLE_KEY])
|
||||
colors = state.widgetColors()
|
||||
}
|
||||
WidgetStylePicker(
|
||||
title = stringResource(R.string.widget_stopwatch_config_title),
|
||||
options = StopwatchWidgetStyle.entries,
|
||||
selected = style,
|
||||
label = { stringResource(stopwatchStyleLabel(it)) },
|
||||
preview = { StopwatchStylePreview(it) },
|
||||
onSelect = { picked ->
|
||||
style = picked
|
||||
val currentStyle = style ?: return
|
||||
val currentColors = colors ?: return
|
||||
StopwatchConfigScreen(
|
||||
style = currentStyle,
|
||||
onStyleChange = { style = it },
|
||||
colors = currentColors,
|
||||
onColorsChange = { colors = it },
|
||||
onSave = {
|
||||
save {
|
||||
updateAppWidgetState(this@StopwatchConfigActivity, glanceId) { prefs ->
|
||||
prefs[WIDGET_STOPWATCH_STYLE_KEY] = picked.name
|
||||
prefs[WIDGET_STOPWATCH_STYLE_KEY] = currentStyle.name
|
||||
prefs.setWidgetColors(currentColors)
|
||||
}
|
||||
StopwatchWidget().update(this@StopwatchConfigActivity, glanceId)
|
||||
}
|
||||
@@ -68,6 +80,45 @@ class StopwatchConfigActivity : WidgetConfigActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StopwatchConfigScreen(
|
||||
style: StopwatchWidgetStyle,
|
||||
onStyleChange: (StopwatchWidgetStyle) -> Unit,
|
||||
colors: WidgetColors,
|
||||
onColorsChange: (WidgetColors) -> Unit,
|
||||
onSave: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
var showStylePicker by remember { mutableStateOf(false) }
|
||||
|
||||
WidgetSettingsScreen(
|
||||
title = stringResource(R.string.widget_stopwatch_config_title),
|
||||
onSave = onSave,
|
||||
onDismiss = onDismiss,
|
||||
preview = { StopwatchStylePreview(style, colors) },
|
||||
) {
|
||||
GroupedRow(
|
||||
title = stringResource(R.string.widget_config_style),
|
||||
summary = stringResource(stopwatchStyleLabel(style)),
|
||||
position = Position.Alone,
|
||||
onClick = { showStylePicker = true },
|
||||
)
|
||||
WidgetColorsSection(colors = colors, onColorsChange = onColorsChange)
|
||||
}
|
||||
|
||||
if (showStylePicker) {
|
||||
WidgetStylePicker(
|
||||
title = stringResource(R.string.widget_config_style),
|
||||
options = StopwatchWidgetStyle.entries,
|
||||
selected = style,
|
||||
label = { stringResource(stopwatchStyleLabel(it)) },
|
||||
preview = { StopwatchStylePreview(it, colors) },
|
||||
onSelect = onStyleChange,
|
||||
onDismiss = { showStylePicker = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun stopwatchStyleLabel(style: StopwatchWidgetStyle): Int = when (style) {
|
||||
StopwatchWidgetStyle.CARD -> R.string.stopwatch_style_card
|
||||
StopwatchWidgetStyle.CIRCLE -> R.string.stopwatch_style_circle
|
||||
@@ -84,13 +135,15 @@ internal fun stopwatchStyleLabel(style: StopwatchWidgetStyle): Int = when (style
|
||||
*/
|
||||
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||
@Composable
|
||||
private fun StopwatchStylePreview(style: StopwatchWidgetStyle) {
|
||||
private fun StopwatchStylePreview(style: StopwatchWidgetStyle, colors: WidgetColors) {
|
||||
val context = LocalContext.current
|
||||
val size = if (style == StopwatchWidgetStyle.CARD) CardPreviewSize else ShapePreviewSize
|
||||
val views by produceState<RemoteViews?>(initialValue = null, style) {
|
||||
val views by produceState<RemoteViews?>(initialValue = null, style, colors) {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
value = GlanceRemoteViews().compose(context, size) {
|
||||
StopwatchWidgetContent(PreviewModel, now, style)
|
||||
CompositionLocalProvider(LocalWidgetColors provides colors) {
|
||||
StopwatchWidgetContent(PreviewModel, now, style)
|
||||
}
|
||||
}.remoteViews
|
||||
}
|
||||
AndroidView(
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package de.jeanlucmakiola.clockula.ui.widget.config
|
||||
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import de.jeanlucmakiola.clockula.R
|
||||
import de.jeanlucmakiola.clockula.widget.TimerWidget
|
||||
|
||||
/** The timer widget's colours. */
|
||||
@AndroidEntryPoint
|
||||
class TimerConfigActivity : WidgetColorsConfigActivity() {
|
||||
override val titleRes: Int = R.string.widget_timer_label
|
||||
override fun widget(): GlanceAppWidget = TimerWidget()
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package de.jeanlucmakiola.clockula.ui.widget.config
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
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.setValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.state.getAppWidgetState
|
||||
import androidx.glance.appwidget.state.updateAppWidgetState
|
||||
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||
import de.jeanlucmakiola.clockula.domain.widget.WidgetColors
|
||||
import de.jeanlucmakiola.clockula.widget.setWidgetColors
|
||||
import de.jeanlucmakiola.clockula.widget.widgetColors
|
||||
|
||||
/**
|
||||
* The config screen of a widget whose only settings are its colours
|
||||
* ([WidgetColorsSection]) — the next-alarm and timer widgets, which had none
|
||||
* before. Both are `configuration_optional`, so placing one never stops here.
|
||||
*/
|
||||
abstract class WidgetColorsConfigActivity : WidgetConfigActivity() {
|
||||
|
||||
@get:StringRes
|
||||
protected abstract val titleRes: Int
|
||||
|
||||
/** A fresh instance of the widget, to redraw the configured one straight after saving. */
|
||||
protected abstract fun widget(): GlanceAppWidget
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
// `null` until read, so neither an early save nor a late load can
|
||||
// write defaults over what the user has (as `ClockWidgetConfigActivity`).
|
||||
var colors by remember { mutableStateOf<WidgetColors?>(null) }
|
||||
LaunchedEffect(glanceId) {
|
||||
colors = getAppWidgetState(this@WidgetColorsConfigActivity, PreferencesGlanceStateDefinition, glanceId).widgetColors()
|
||||
}
|
||||
val current = colors ?: return
|
||||
WidgetSettingsScreen(
|
||||
title = stringResource(titleRes),
|
||||
onSave = {
|
||||
save {
|
||||
updateAppWidgetState(this@WidgetColorsConfigActivity, glanceId) { prefs -> prefs.setWidgetColors(current) }
|
||||
widget().update(this@WidgetColorsConfigActivity, glanceId)
|
||||
}
|
||||
},
|
||||
onDismiss = ::dismiss,
|
||||
) {
|
||||
WidgetColorsSection(colors = current, onColorsChange = { colors = it }, gapAbove = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package de.jeanlucmakiola.clockula.ui.widget.config
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
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.unit.dp
|
||||
import de.jeanlucmakiola.clockula.R
|
||||
import de.jeanlucmakiola.clockula.domain.widget.WidgetColorTheme
|
||||
import de.jeanlucmakiola.clockula.domain.widget.WidgetColors
|
||||
import de.jeanlucmakiola.floret.components.FullScreenPicker
|
||||
import de.jeanlucmakiola.floret.components.GroupedRow
|
||||
import de.jeanlucmakiola.floret.components.OptionPicker
|
||||
import de.jeanlucmakiola.floret.components.Position
|
||||
|
||||
/** Android 12+: where wallpaper colours exist at all, and where the plain-`RemoteViews` clock faces can be re-coloured. */
|
||||
internal val WidgetDynamicColorAvailable: Boolean get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
|
||||
/**
|
||||
* A widget config screen with settings rather than a single pick: the
|
||||
* widget's [preview], if it has one, above its rows, and an OK that [onSave]s
|
||||
* them all at once — the shape `ClockWidgetConfigActivity` set.
|
||||
*/
|
||||
@Composable
|
||||
internal fun WidgetSettingsScreen(
|
||||
title: String,
|
||||
onSave: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
preview: (@Composable () -> Unit)? = null,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
FullScreenPicker(
|
||||
title = title,
|
||||
onDismiss = onDismiss,
|
||||
actions = {
|
||||
TextButton(onClick = onSave) { Text(stringResource(android.R.string.ok)) }
|
||||
},
|
||||
) {
|
||||
if (preview != null) {
|
||||
// The widget as it will look, live, above the settings that change it.
|
||||
Box(modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), contentAlignment = Alignment.Center) {
|
||||
preview()
|
||||
}
|
||||
}
|
||||
content()
|
||||
Spacer(Modifier.height(24.dp))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A widget instance's colours, as the app's own Appearance screen offers
|
||||
* them: the theme — follow the system, or pin Light or Dark — and, on
|
||||
* Android 12+, whether to take the wallpaper's colours. Its own group, set
|
||||
* off from the widget's other settings above it by a gap unless [gapAbove]
|
||||
* is off (a screen with nothing else on it).
|
||||
*
|
||||
* Nothing when not [available]: the analog and digital clocks can only be
|
||||
* re-coloured from API 31 (`AnalogClockWidgetContent.kt`, `DigitalFaces.kt`).
|
||||
*/
|
||||
@Composable
|
||||
internal fun WidgetColorsSection(
|
||||
colors: WidgetColors,
|
||||
onColorsChange: (WidgetColors) -> Unit,
|
||||
available: Boolean = true,
|
||||
gapAbove: Boolean = true,
|
||||
) {
|
||||
if (!available) return
|
||||
if (gapAbove) Spacer(Modifier.height(16.dp))
|
||||
var showTheme by rememberSaveable { mutableStateOf(false) }
|
||||
val dynamic = WidgetDynamicColorAvailable
|
||||
|
||||
GroupedRow(
|
||||
title = stringResource(R.string.settings_theme),
|
||||
summary = widgetThemeLabel(colors.theme),
|
||||
position = if (dynamic) Position.Top else Position.Alone,
|
||||
onClick = { showTheme = true },
|
||||
)
|
||||
// Below Android 12 there are no wallpaper colours to switch off.
|
||||
if (dynamic) {
|
||||
GroupedRow(
|
||||
title = stringResource(R.string.settings_dynamic_color),
|
||||
summary = stringResource(R.string.settings_dynamic_color_hint),
|
||||
position = Position.Bottom,
|
||||
trailing = {
|
||||
Switch(
|
||||
checked = colors.wallpaperColors,
|
||||
onCheckedChange = { onColorsChange(colors.copy(wallpaperColors = it)) },
|
||||
)
|
||||
},
|
||||
onClick = { onColorsChange(colors.copy(wallpaperColors = !colors.wallpaperColors)) },
|
||||
)
|
||||
}
|
||||
|
||||
if (showTheme) {
|
||||
OptionPicker(
|
||||
title = stringResource(R.string.settings_theme),
|
||||
options = WidgetColorTheme.entries.toList(),
|
||||
selected = colors.theme,
|
||||
label = { widgetThemeLabel(it) },
|
||||
onSelect = { onColorsChange(colors.copy(theme = it)) },
|
||||
onDismiss = { showTheme = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun widgetThemeLabel(theme: WidgetColorTheme): String = stringResource(
|
||||
when (theme) {
|
||||
WidgetColorTheme.SYSTEM -> R.string.settings_theme_system
|
||||
WidgetColorTheme.LIGHT -> R.string.settings_theme_light
|
||||
WidgetColorTheme.DARK -> R.string.settings_theme_dark
|
||||
},
|
||||
)
|
||||
@@ -8,6 +8,7 @@ import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||
import androidx.glance.appwidget.SizeMode
|
||||
import androidx.glance.appwidget.provideContent
|
||||
import de.jeanlucmakiola.clockula.ui.widget.ProvideWidgetColors
|
||||
import de.jeanlucmakiola.clockula.ui.widget.AnalogClockWidgetRoot
|
||||
|
||||
/**
|
||||
@@ -29,7 +30,7 @@ class AnalogClockWidget : GlanceAppWidget() {
|
||||
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
provideContent {
|
||||
AnalogClockWidgetRoot()
|
||||
ProvideWidgetColors { AnalogClockWidgetRoot() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||
import androidx.glance.appwidget.SizeMode
|
||||
import androidx.glance.appwidget.provideContent
|
||||
import de.jeanlucmakiola.clockula.ui.widget.ProvideWidgetColors
|
||||
import de.jeanlucmakiola.clockula.ui.widget.ClockWidgetRoot
|
||||
import de.jeanlucmakiola.clockula.ui.widget.widgetCities
|
||||
import de.jeanlucmakiola.clockula.widget.di.widgetEntryPoint
|
||||
@@ -36,7 +37,7 @@ class ClockWidget : GlanceAppWidget() {
|
||||
val entryPoint = widgetEntryPoint(context)
|
||||
val initialCities = widgetCities(entryPoint.worldClockRepository(), entryPoint.zoneNames())
|
||||
provideContent {
|
||||
ClockWidgetRoot(entryPoint, initialCities)
|
||||
ProvideWidgetColors { ClockWidgetRoot(entryPoint, initialCities) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package de.jeanlucmakiola.clockula.widget
|
||||
|
||||
import androidx.datastore.preferences.core.MutablePreferences
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import de.jeanlucmakiola.clockula.domain.widget.AnalogClockStyle
|
||||
import de.jeanlucmakiola.clockula.domain.widget.DigitalClockSettings
|
||||
import de.jeanlucmakiola.clockula.domain.widget.DigitalClockStyle
|
||||
import de.jeanlucmakiola.clockula.domain.widget.WidgetColorTheme
|
||||
import de.jeanlucmakiola.clockula.domain.widget.WidgetColors
|
||||
import de.jeanlucmakiola.clockula.domain.widget.parseDigitalClockStyle
|
||||
import de.jeanlucmakiola.clockula.domain.widget.parseWidgetColors
|
||||
|
||||
/** Per-instance Glance state key holding the digital widget's [DigitalClockStyle] ([name]). */
|
||||
internal val WIDGET_DIGITAL_STYLE_KEY = stringPreferencesKey("digital_style")
|
||||
@@ -41,3 +46,17 @@ internal fun parseDigitalClockSettings(style: String?, showDate: Boolean?, showW
|
||||
showDate = showDate ?: true,
|
||||
showWorldClocks = showWorldClocks ?: false,
|
||||
)
|
||||
|
||||
/** Per-instance Glance state key holding any widget's [WidgetColorTheme] ([name]); every widget has its colours. */
|
||||
internal val WIDGET_THEME_KEY = stringPreferencesKey("widget_theme")
|
||||
|
||||
/** Per-instance Glance state key: use the wallpaper's colours (Android 12+) rather than the app's own. */
|
||||
internal val WIDGET_WALLPAPER_COLORS_KEY = booleanPreferencesKey("widget_wallpaper_colors")
|
||||
|
||||
/** This instance's [WidgetColors]; an instance that never saved any gets the defaults, i.e. the look from before they existed. */
|
||||
internal fun Preferences.widgetColors(): WidgetColors = parseWidgetColors(this[WIDGET_THEME_KEY], this[WIDGET_WALLPAPER_COLORS_KEY])
|
||||
|
||||
internal fun MutablePreferences.setWidgetColors(colors: WidgetColors) {
|
||||
this[WIDGET_THEME_KEY] = colors.theme.name
|
||||
this[WIDGET_WALLPAPER_COLORS_KEY] = colors.wallpaperColors
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||
import androidx.glance.appwidget.SizeMode
|
||||
import androidx.glance.appwidget.provideContent
|
||||
import de.jeanlucmakiola.clockula.ui.widget.ProvideWidgetColors
|
||||
import de.jeanlucmakiola.clockula.ui.widget.NextAlarmWidgetRoot
|
||||
import de.jeanlucmakiola.clockula.widget.di.widgetEntryPoint
|
||||
import kotlinx.coroutines.flow.first
|
||||
@@ -20,7 +21,7 @@ class NextAlarmWidget : GlanceAppWidget() {
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
val entryPoint = widgetEntryPoint(context)
|
||||
val initial = entryPoint.alarmEngine().upcoming().first()
|
||||
provideContent { NextAlarmWidgetRoot(entryPoint, initial) }
|
||||
provideContent { ProvideWidgetColors { NextAlarmWidgetRoot(entryPoint, initial) } }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||
import androidx.glance.appwidget.SizeMode
|
||||
import androidx.glance.appwidget.provideContent
|
||||
import de.jeanlucmakiola.clockula.ui.widget.ProvideWidgetColors
|
||||
import de.jeanlucmakiola.clockula.ui.widget.QuickTimerWidgetRoot
|
||||
import de.jeanlucmakiola.clockula.widget.di.widgetEntryPoint
|
||||
import kotlinx.coroutines.flow.first
|
||||
@@ -27,7 +28,7 @@ class QuickTimerWidget : GlanceAppWidget() {
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
val entryPoint = widgetEntryPoint(context)
|
||||
val initial = entryPoint.timerRepository().timers().first()
|
||||
provideContent { QuickTimerWidgetRoot(entryPoint, initial) }
|
||||
provideContent { ProvideWidgetColors { QuickTimerWidgetRoot(entryPoint, initial) } }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||
import androidx.glance.appwidget.SizeMode
|
||||
import androidx.glance.appwidget.provideContent
|
||||
import de.jeanlucmakiola.clockula.ui.widget.ProvideWidgetColors
|
||||
import de.jeanlucmakiola.clockula.ui.widget.StopwatchWidgetRoot
|
||||
import de.jeanlucmakiola.clockula.widget.di.widgetEntryPoint
|
||||
import kotlinx.coroutines.flow.first
|
||||
@@ -22,7 +23,7 @@ class StopwatchWidget : GlanceAppWidget() {
|
||||
val repository = entryPoint.stopwatchRepository()
|
||||
val initialRun = repository.run().first()
|
||||
val initialLaps = repository.laps().first()
|
||||
provideContent { StopwatchWidgetRoot(entryPoint, initialRun, initialLaps.size) }
|
||||
provideContent { ProvideWidgetColors { StopwatchWidgetRoot(entryPoint, initialRun, initialLaps.size) } }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||
import androidx.glance.appwidget.SizeMode
|
||||
import androidx.glance.appwidget.provideContent
|
||||
import de.jeanlucmakiola.clockula.ui.widget.ProvideWidgetColors
|
||||
import de.jeanlucmakiola.clockula.ui.widget.TimerWidgetRoot
|
||||
import de.jeanlucmakiola.clockula.widget.di.widgetEntryPoint
|
||||
import kotlinx.coroutines.flow.first
|
||||
@@ -21,7 +22,7 @@ class TimerWidget : GlanceAppWidget() {
|
||||
val entryPoint = widgetEntryPoint(context)
|
||||
val repository = entryPoint.timerRepository()
|
||||
val initial = repository.timers().first()
|
||||
provideContent { TimerWidgetRoot(entryPoint, initial) }
|
||||
provideContent { ProvideWidgetColors { TimerWidgetRoot(entryPoint, initial) } }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/widget_digital_shape"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:importantForAccessibility="no"
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
<resources>
|
||||
<!-- Dark-mode counterparts of the analog widget's colours — see the light
|
||||
`values/colors.xml` for why these are literal, resource-qualified
|
||||
colours rather than `?android:attr/...`. -->
|
||||
<color name="widget_analog_dial_fill">#FF2B2F26</color>
|
||||
<color name="widget_analog_dial_primary">#FF3E4C32</color>
|
||||
<color name="widget_analog_dial_tertiary">#FF334B47</color>
|
||||
<color name="widget_analog_hand_hour">#FFE4E9E0</color>
|
||||
<color name="widget_analog_hand_minute">#FFB4C7A4</color>
|
||||
<color name="widget_analog_numeral">#FFC9D9B9</color>
|
||||
<color name="widget_analog_second">#FFA5CEC7</color>
|
||||
<color name="widget_analog_date">#FFE3E3DC</color>
|
||||
<color name="widget_analog_badge_pill">#FFB4C7A4</color>
|
||||
<color name="widget_analog_badge_pill_text">#FF22261C</color>
|
||||
<!-- Dark-mode counterparts of the analog widget's app-palette colours —
|
||||
see the light `values/colors.xml` for why these are literal,
|
||||
resource-qualified colours rather than `?android:attr/...`. -->
|
||||
<color name="widget_app_analog_dial_fill">#FF2B2F26</color>
|
||||
<color name="widget_app_analog_dial_primary">#FF3E4C32</color>
|
||||
<color name="widget_app_analog_dial_tertiary">#FF334B47</color>
|
||||
<color name="widget_app_analog_hand_hour">#FFE4E9E0</color>
|
||||
<color name="widget_app_analog_hand_minute">#FFB4C7A4</color>
|
||||
<color name="widget_app_analog_numeral">#FFC9D9B9</color>
|
||||
<color name="widget_app_analog_second">#FFA5CEC7</color>
|
||||
<color name="widget_app_analog_date">#FFE3E3DC</color>
|
||||
<color name="widget_app_analog_badge_pill">#FFB4C7A4</color>
|
||||
<color name="widget_app_analog_badge_pill_text">#FF22261C</color>
|
||||
|
||||
<!-- Dark tones of the shared widget palette in `values/colors.xml`. -->
|
||||
<color name="widget_surface">#FF2B2F26</color>
|
||||
<color name="widget_on_surface">#FFE4E9E0</color>
|
||||
<color name="widget_on_surface_variant">#FFC4C8BA</color>
|
||||
<color name="widget_primary">#FFCEE2BA</color>
|
||||
<color name="widget_primary_container">#FF4A583B</color>
|
||||
<color name="widget_on_primary_container">#FFE8F6D9</color>
|
||||
<color name="widget_on_primary">#FF213510</color>
|
||||
<color name="widget_secondary_container">#FF3D4A35</color>
|
||||
<color name="widget_on_secondary_container">#FFD8E7CB</color>
|
||||
<color name="widget_tertiary">#FFA5CEC7</color>
|
||||
<color name="widget_tertiary_container">#FF244D48</color>
|
||||
<color name="widget_on_tertiary_container">#FFC1ECE4</color>
|
||||
<color name="widget_app_surface">#FF2B2F26</color>
|
||||
<color name="widget_app_on_surface">#FFE4E9E0</color>
|
||||
<color name="widget_app_on_surface_variant">#FFC4C8BA</color>
|
||||
<color name="widget_app_primary">#FFCEE2BA</color>
|
||||
<color name="widget_app_primary_container">#FF4A583B</color>
|
||||
<color name="widget_app_on_primary_container">#FFE8F6D9</color>
|
||||
<color name="widget_app_on_primary">#FF213510</color>
|
||||
<color name="widget_app_secondary_container">#FF3D4A35</color>
|
||||
<color name="widget_app_on_secondary_container">#FFD8E7CB</color>
|
||||
<color name="widget_app_tertiary">#FFA5CEC7</color>
|
||||
<color name="widget_app_tertiary_container">#FF244D48</color>
|
||||
<color name="widget_app_on_tertiary_container">#FFC1ECE4</color>
|
||||
</resources>
|
||||
|
||||
@@ -12,32 +12,60 @@
|
||||
resolved against the host process's own configuration (`values-night/`
|
||||
below), the same mechanism the platform itself uses for day/night —
|
||||
not a `?android:attr`, which proved unreliable here. -->
|
||||
<color name="widget_analog_dial_fill">#FFE8EAE1</color>
|
||||
<color name="widget_analog_dial_primary">#FFD7E8C6</color>
|
||||
<color name="widget_analog_dial_tertiary">#FFCCE8E3</color>
|
||||
<color name="widget_analog_hand_hour">#FF44483E</color>
|
||||
<color name="widget_analog_hand_minute">#FF6B7A5C</color>
|
||||
<color name="widget_analog_numeral">#FF4E5C40</color>
|
||||
<color name="widget_analog_second">#FF3D6660</color>
|
||||
<color name="widget_analog_date">#FF1A1C18</color>
|
||||
<color name="widget_analog_badge_pill">#FF6B7A5C</color>
|
||||
<color name="widget_analog_badge_pill_text">#FFFFFFFF</color>
|
||||
<color name="widget_app_analog_dial_fill">#FFE8EAE1</color>
|
||||
<color name="widget_app_analog_dial_primary">#FFD7E8C6</color>
|
||||
<color name="widget_app_analog_dial_tertiary">#FFCCE8E3</color>
|
||||
<color name="widget_app_analog_hand_hour">#FF44483E</color>
|
||||
<color name="widget_app_analog_hand_minute">#FF6B7A5C</color>
|
||||
<color name="widget_app_analog_numeral">#FF4E5C40</color>
|
||||
<color name="widget_app_analog_second">#FF3D6660</color>
|
||||
<color name="widget_app_analog_date">#FF1A1C18</color>
|
||||
<color name="widget_app_analog_badge_pill">#FF6B7A5C</color>
|
||||
<color name="widget_app_analog_badge_pill_text">#FFFFFFFF</color>
|
||||
|
||||
<!-- Every other widget's palette (light), named after the M3 roles they
|
||||
stand in for. Like the analog colours above, resource-qualified
|
||||
rather than `?android:attr/...` or a Glance theme, so the launcher
|
||||
resolves them against its own day/night (`values-night/`), and on
|
||||
API 31+ against the wallpaper (`values-v31/`). -->
|
||||
<color name="widget_surface">#FFE8EAE1</color>
|
||||
<color name="widget_on_surface">#FF1C1F1A</color>
|
||||
<color name="widget_on_surface_variant">#FF44483E</color>
|
||||
<color name="widget_primary">#FF5C6A4E</color>
|
||||
<color name="widget_primary_container">#FFD7E8C6</color>
|
||||
<color name="widget_on_primary_container">#FF1D2A10</color>
|
||||
<color name="widget_on_primary">#FFFFFFFF</color>
|
||||
<color name="widget_secondary_container">#FFDCE7D0</color>
|
||||
<color name="widget_on_secondary_container">#FF161E10</color>
|
||||
<color name="widget_tertiary">#FF3D6660</color>
|
||||
<color name="widget_tertiary_container">#FFCCE8E3</color>
|
||||
<color name="widget_on_tertiary_container">#FF05201C</color>
|
||||
<color name="widget_app_surface">#FFE8EAE1</color>
|
||||
<color name="widget_app_on_surface">#FF1C1F1A</color>
|
||||
<color name="widget_app_on_surface_variant">#FF44483E</color>
|
||||
<color name="widget_app_primary">#FF5C6A4E</color>
|
||||
<color name="widget_app_primary_container">#FFD7E8C6</color>
|
||||
<color name="widget_app_on_primary_container">#FF1D2A10</color>
|
||||
<color name="widget_app_on_primary">#FFFFFFFF</color>
|
||||
<color name="widget_app_secondary_container">#FFDCE7D0</color>
|
||||
<color name="widget_app_on_secondary_container">#FF161E10</color>
|
||||
<color name="widget_app_tertiary">#FF3D6660</color>
|
||||
<color name="widget_app_tertiary_container">#FFCCE8E3</color>
|
||||
<color name="widget_app_on_tertiary_container">#FF05201C</color>
|
||||
|
||||
<!-- What the widgets' layouts and drawables actually reference. Below
|
||||
API 31 these are just the app palette above; `values-v31/` points
|
||||
them at the wallpaper's tones instead. The `widget_app_*` tones stay
|
||||
reachable on every API level, so a widget whose wallpaper colours are
|
||||
switched off can still resolve them (`ui/widget/WidgetPalette.kt`). -->
|
||||
<color name="widget_analog_dial_fill">@color/widget_app_analog_dial_fill</color>
|
||||
<color name="widget_analog_dial_primary">@color/widget_app_analog_dial_primary</color>
|
||||
<color name="widget_analog_dial_tertiary">@color/widget_app_analog_dial_tertiary</color>
|
||||
<color name="widget_analog_hand_hour">@color/widget_app_analog_hand_hour</color>
|
||||
<color name="widget_analog_hand_minute">@color/widget_app_analog_hand_minute</color>
|
||||
<color name="widget_analog_numeral">@color/widget_app_analog_numeral</color>
|
||||
<color name="widget_analog_second">@color/widget_app_analog_second</color>
|
||||
<color name="widget_analog_date">@color/widget_app_analog_date</color>
|
||||
<color name="widget_analog_badge_pill">@color/widget_app_analog_badge_pill</color>
|
||||
<color name="widget_analog_badge_pill_text">@color/widget_app_analog_badge_pill_text</color>
|
||||
<color name="widget_surface">@color/widget_app_surface</color>
|
||||
<color name="widget_on_surface">@color/widget_app_on_surface</color>
|
||||
<color name="widget_on_surface_variant">@color/widget_app_on_surface_variant</color>
|
||||
<color name="widget_primary">@color/widget_app_primary</color>
|
||||
<color name="widget_primary_container">@color/widget_app_primary_container</color>
|
||||
<color name="widget_on_primary_container">@color/widget_app_on_primary_container</color>
|
||||
<color name="widget_on_primary">@color/widget_app_on_primary</color>
|
||||
<color name="widget_secondary_container">@color/widget_app_secondary_container</color>
|
||||
<color name="widget_on_secondary_container">@color/widget_app_on_secondary_container</color>
|
||||
<color name="widget_tertiary">@color/widget_app_tertiary</color>
|
||||
<color name="widget_tertiary_container">@color/widget_app_tertiary_container</color>
|
||||
<color name="widget_on_tertiary_container">@color/widget_app_on_tertiary_container</color>
|
||||
</resources>
|
||||
|
||||
@@ -10,4 +10,6 @@
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:widgetCategory="home_screen"
|
||||
android:previewImage="@mipmap/ic_launcher"
|
||||
android:previewLayout="@layout/widget_preview_next_alarm" />
|
||||
android:previewLayout="@layout/widget_preview_next_alarm"
|
||||
android:configure="de.jeanlucmakiola.clockula.ui.widget.config.NextAlarmConfigActivity"
|
||||
android:widgetFeatures="reconfigurable|configuration_optional" />
|
||||
|
||||
@@ -11,4 +11,6 @@
|
||||
android:widgetCategory="home_screen"
|
||||
android:previewImage="@mipmap/ic_launcher"
|
||||
android:previewLayout="@layout/widget_preview_timer"
|
||||
android:description="@string/widget_timer_description" />
|
||||
android:description="@string/widget_timer_description"
|
||||
android:configure="de.jeanlucmakiola.clockula.ui.widget.config.TimerConfigActivity"
|
||||
android:widgetFeatures="reconfigurable|configuration_optional" />
|
||||
|
||||
@@ -31,7 +31,7 @@ class ManifestRulesTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exactly the launcher and its two icon aliases, the door, the four widget configurators and the six widget receivers are exported`() {
|
||||
fun `exactly the launcher and its two icon aliases, the door, the six widget configurators and the six widget receivers are exported`() {
|
||||
val exported = Regex("<(activity|service|receiver|provider)\\b[\\s\\S]*?(?:/>|</\\1>)")
|
||||
.findAll(text)
|
||||
.map { it.value }
|
||||
@@ -42,10 +42,11 @@ class ManifestRulesTest {
|
||||
// A GlanceAppWidgetReceiver must be exported for the platform to pick
|
||||
// it as a widget provider at all, and `the six widget receivers
|
||||
// answer only APPWIDGET_UPDATE` pins that each one is guarded to
|
||||
// that one platform-delivered broadcast and nothing wider. M11 adds
|
||||
// four widget config activities: exported for the same reason —
|
||||
// that one platform-delivered broadcast and nothing wider. The six
|
||||
// widget config activities (M11's four, plus the next-alarm and
|
||||
// timer widgets' colour screens) are exported for the same reason —
|
||||
// only the launcher can start an `APPWIDGET_CONFIGURE` activity
|
||||
// cross-package — and guarded to that one action by `the four
|
||||
// cross-package — and guarded to that one action by `the six
|
||||
// widget configurators answer only APPWIDGET_CONFIGURE` below.
|
||||
assertThat(exported).containsExactly(
|
||||
".MainActivity",
|
||||
@@ -56,6 +57,8 @@ class ManifestRulesTest {
|
||||
".ui.widget.config.AnalogClockConfigActivity",
|
||||
".ui.widget.config.StopwatchConfigActivity",
|
||||
".ui.widget.config.QuickTimerConfigActivity",
|
||||
".ui.widget.config.NextAlarmConfigActivity",
|
||||
".ui.widget.config.TimerConfigActivity",
|
||||
".widget.NextAlarmWidgetReceiver",
|
||||
".widget.TimerWidgetReceiver",
|
||||
".widget.ClockWidgetReceiver",
|
||||
@@ -67,16 +70,18 @@ class ManifestRulesTest {
|
||||
|
||||
/**
|
||||
* M11: the mirror of `the six widget receivers answer only
|
||||
* APPWIDGET_UPDATE` for the four widget config activities — being
|
||||
* APPWIDGET_UPDATE` for the six widget config activities — being
|
||||
* exported cannot be widened into a second, wider surface by accident.
|
||||
*/
|
||||
@Test
|
||||
fun `the four widget configurators answer only APPWIDGET_CONFIGURE`() {
|
||||
fun `the six widget configurators answer only APPWIDGET_CONFIGURE`() {
|
||||
val configurators = listOf(
|
||||
".ui.widget.config.ClockWidgetConfigActivity",
|
||||
".ui.widget.config.AnalogClockConfigActivity",
|
||||
".ui.widget.config.StopwatchConfigActivity",
|
||||
".ui.widget.config.QuickTimerConfigActivity",
|
||||
".ui.widget.config.NextAlarmConfigActivity",
|
||||
".ui.widget.config.TimerConfigActivity",
|
||||
)
|
||||
for (name in configurators) {
|
||||
val block = activityBlock(name)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package de.jeanlucmakiola.clockula.domain.widget
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class WidgetColorsTest {
|
||||
|
||||
@Test
|
||||
fun `nothing stored is today's look`() {
|
||||
assertThat(parseWidgetColors(null, null)).isEqualTo(WidgetColors(WidgetColorTheme.SYSTEM, wallpaperColors = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an unknown theme name falls back to System`() {
|
||||
assertThat(parseWidgetColors("Sepia", false)).isEqualTo(WidgetColors(WidgetColorTheme.SYSTEM, wallpaperColors = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `each theme's own name round-trips`() {
|
||||
WidgetColorTheme.entries.forEach { theme ->
|
||||
assertThat(parseWidgetColors(theme.name, true).theme).isEqualTo(theme)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the defaults leave the palette to the launcher on every API level`() {
|
||||
assertThat(WidgetColors().paletteMode(dynamicAvailable = true)).isEqualTo(WidgetPaletteMode.Launcher)
|
||||
assertThat(WidgetColors().paletteMode(dynamicAvailable = false)).isEqualTo(WidgetPaletteMode.Launcher)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `wallpaper colours off under System uses the app palette, day and night`() {
|
||||
val colors = WidgetColors(WidgetColorTheme.SYSTEM, wallpaperColors = false)
|
||||
assertThat(colors.paletteMode(dynamicAvailable = true)).isEqualTo(WidgetPaletteMode.AppDayNight)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `without wallpaper colours on the device, the switch changes nothing`() {
|
||||
val colors = WidgetColors(WidgetColorTheme.SYSTEM, wallpaperColors = false)
|
||||
assertThat(colors.paletteMode(dynamicAvailable = false)).isEqualTo(WidgetPaletteMode.Launcher)
|
||||
assertThat(WidgetColors(WidgetColorTheme.DARK, wallpaperColors = true).paletteMode(dynamicAvailable = false))
|
||||
.isEqualTo(WidgetPaletteMode.Pinned(dark = true, dynamic = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `light and dark pin a tone of the chosen palette`() {
|
||||
assertThat(WidgetColors(WidgetColorTheme.LIGHT, wallpaperColors = true).paletteMode(dynamicAvailable = true))
|
||||
.isEqualTo(WidgetPaletteMode.Pinned(dark = false, dynamic = true))
|
||||
assertThat(WidgetColors(WidgetColorTheme.DARK, wallpaperColors = false).paletteMode(dynamicAvailable = true))
|
||||
.isEqualTo(WidgetPaletteMode.Pinned(dark = true, dynamic = false))
|
||||
}
|
||||
}
|
||||
@@ -139,7 +139,7 @@ kit module — the kit's roadmap keeps schedulers app-local.
|
||||
| `ui/worldclock/` | the real World clock tab (M8): the pure row builders (`WorldClockRows`, `ZonePickerRows`) and their state, `WorldClockSource`, the ViewModel, the tab, the analog face, the row and the zone picker, `WorldClockDefaults` |
|
||||
| `ui/ring/` | `AlarmRingActivity`, its ViewModel, `RingScreen` and the dismiss-challenge controls |
|
||||
| `ui/settings/` | the settings hub (M10): `SettingsScreen`/`SettingsViewModel` (appearance, alarm/timer/clock defaults, reliability, data, help), `SelfCheckScreen`/`SelfCheckViewModel`, `BackupScreen`/`BackupViewModel` — all reachable from a gear icon on every tab, with no owning tab of their own |
|
||||
| `ui/widget/` | the three widgets' `@Composable` Glance content (M10) — the only Compose this milestone adds outside a tab; the `GlanceAppWidget`/`GlanceAppWidgetReceiver` classes themselves live in `widget/`, since they are manifest components, not UI |
|
||||
| `ui/widget/` | the three widgets' `@Composable` Glance content (M10) — the only Compose this milestone adds outside a tab; the `GlanceAppWidget`/`GlanceAppWidgetReceiver` classes themselves live in `widget/`, since they are manifest components, not UI. `ui/widget/config/` holds each widget's config activity; every widget has one, if only for its per-instance colours (`domain/widget/WidgetColors.kt`: System/Light/Dark and, on API 31+, wallpaper colours on/off). Glance content takes those through `WidgetTheme`; the plain-`RemoteViews` faces, readouts and chronometers through `WidgetPalette.kt`, which resolves the `widget_*` palette under a forced day/night (the app's own tones are `widget_app_*`) and sets concrete colours, or day/night pairs, on the views. The analog and digital clocks are re-coloured on API 31+ only, since below that `AnalogClock`'s hands and a shape's background tint can't be set through `RemoteViews` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user