feat(widgets): stopwatch style picker and widget-picker previews
The stopwatch widget gets a per-instance style, chosen in a new StopwatchConfigActivity on the APPWIDGET_CONFIGURE contract. Every widget receiver now carries its own label, with a separate description, and the widget picker shows sample-value preview layouts instead of a bare name.
This commit is contained in:
@@ -258,8 +258,19 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<!-- The stopwatch widget's per-instance style picker: the same
|
||||||
|
APPWIDGET_CONFIGURE contract as the two above. -->
|
||||||
|
<activity
|
||||||
|
android:name=".ui.widget.config.StopwatchConfigActivity"
|
||||||
|
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 per-instance duration picker (M11): the
|
<!-- The quick-timer widget's per-instance duration picker (M11): the
|
||||||
same APPWIDGET_CONFIGURE contract as the two above. -->
|
same APPWIDGET_CONFIGURE contract as the three above. -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.widget.config.QuickTimerConfigActivity"
|
android:name=".ui.widget.config.QuickTimerConfigActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -289,6 +300,7 @@
|
|||||||
change (widget/AnalogDateRefresh.kt). -->
|
change (widget/AnalogDateRefresh.kt). -->
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".widget.NextAlarmWidgetReceiver"
|
android:name=".widget.NextAlarmWidgetReceiver"
|
||||||
|
android:label="@string/widget_next_alarm_label"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
@@ -300,6 +312,7 @@
|
|||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".widget.TimerWidgetReceiver"
|
android:name=".widget.TimerWidgetReceiver"
|
||||||
|
android:label="@string/widget_timer_label"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
@@ -311,6 +324,7 @@
|
|||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".widget.ClockWidgetReceiver"
|
android:name=".widget.ClockWidgetReceiver"
|
||||||
|
android:label="@string/widget_clock_label"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
@@ -322,6 +336,7 @@
|
|||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".widget.AnalogClockWidgetReceiver"
|
android:name=".widget.AnalogClockWidgetReceiver"
|
||||||
|
android:label="@string/widget_analog_label"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
@@ -333,6 +348,7 @@
|
|||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".widget.StopwatchWidgetReceiver"
|
android:name=".widget.StopwatchWidgetReceiver"
|
||||||
|
android:label="@string/widget_stopwatch_label"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
@@ -344,6 +360,7 @@
|
|||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".widget.QuickTimerWidgetReceiver"
|
android:name=".widget.QuickTimerWidgetReceiver"
|
||||||
|
android:label="@string/widget_quick_timer_label"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package de.jeanlucmakiola.clockula.domain.widget
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The faces a [de.jeanlucmakiola.clockula.widget.StopwatchWidget] instance can
|
||||||
|
* render. [CARD] is the original full-widget card; every other style is an M3
|
||||||
|
* Expressive shape floating on the home screen, with the start/pause button
|
||||||
|
* tucked against its lower-left edge and lap/reset in a pill at its upper
|
||||||
|
* right. See `ui/widget/StopwatchWidgetContent.kt` for the mapping.
|
||||||
|
*/
|
||||||
|
enum class StopwatchWidgetStyle { CARD, CIRCLE, COOKIE, SUNNY, SQUARE }
|
||||||
|
|
||||||
|
/** Anything unreadable, or a name this enum no longer has, falls back to [StopwatchWidgetStyle.CARD] — what every instance placed before styles existed shows. */
|
||||||
|
fun parseStopwatchWidgetStyle(raw: String?): StopwatchWidgetStyle =
|
||||||
|
raw?.let { runCatching { StopwatchWidgetStyle.valueOf(it) }.getOrNull() } ?: StopwatchWidgetStyle.CARD
|
||||||
@@ -2,21 +2,31 @@ package de.jeanlucmakiola.clockula.ui.widget
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.glance.ColorFilter
|
||||||
import androidx.glance.GlanceModifier
|
import androidx.glance.GlanceModifier
|
||||||
import androidx.glance.GlanceTheme
|
import androidx.glance.GlanceTheme
|
||||||
|
import androidx.glance.Image
|
||||||
|
import androidx.glance.ImageProvider
|
||||||
import androidx.glance.LocalContext
|
import androidx.glance.LocalContext
|
||||||
import androidx.glance.LocalSize
|
import androidx.glance.LocalSize
|
||||||
|
import androidx.glance.action.clickable
|
||||||
import androidx.glance.appwidget.action.actionSendBroadcast
|
import androidx.glance.appwidget.action.actionSendBroadcast
|
||||||
import androidx.glance.appwidget.action.actionStartActivity
|
import androidx.glance.appwidget.action.actionStartActivity
|
||||||
|
import androidx.glance.background
|
||||||
|
import androidx.glance.currentState
|
||||||
import androidx.glance.layout.Alignment
|
import androidx.glance.layout.Alignment
|
||||||
|
import androidx.glance.layout.Box
|
||||||
import androidx.glance.layout.Column
|
import androidx.glance.layout.Column
|
||||||
import androidx.glance.layout.Row
|
import androidx.glance.layout.Row
|
||||||
import androidx.glance.layout.Spacer
|
import androidx.glance.layout.Spacer
|
||||||
import androidx.glance.layout.fillMaxSize
|
import androidx.glance.layout.fillMaxSize
|
||||||
import androidx.glance.layout.height
|
import androidx.glance.layout.height
|
||||||
import androidx.glance.layout.padding
|
import androidx.glance.layout.padding
|
||||||
|
import androidx.glance.layout.size
|
||||||
import androidx.glance.layout.width
|
import androidx.glance.layout.width
|
||||||
import androidx.glance.text.FontWeight
|
import androidx.glance.text.FontWeight
|
||||||
import androidx.glance.text.Text
|
import androidx.glance.text.Text
|
||||||
@@ -26,6 +36,9 @@ import de.jeanlucmakiola.clockula.R
|
|||||||
import de.jeanlucmakiola.clockula.domain.StopwatchState
|
import de.jeanlucmakiola.clockula.domain.StopwatchState
|
||||||
import de.jeanlucmakiola.clockula.domain.format.ClockFormat
|
import de.jeanlucmakiola.clockula.domain.format.ClockFormat
|
||||||
import de.jeanlucmakiola.clockula.domain.widget.StopwatchWidgetModel
|
import de.jeanlucmakiola.clockula.domain.widget.StopwatchWidgetModel
|
||||||
|
import de.jeanlucmakiola.clockula.domain.widget.StopwatchWidgetStyle
|
||||||
|
import de.jeanlucmakiola.clockula.domain.widget.parseStopwatchWidgetStyle
|
||||||
|
import de.jeanlucmakiola.clockula.widget.WIDGET_STOPWATCH_STYLE_KEY
|
||||||
import de.jeanlucmakiola.clockula.stopwatch.StopwatchIntents
|
import de.jeanlucmakiola.clockula.stopwatch.StopwatchIntents
|
||||||
import de.jeanlucmakiola.clockula.stopwatch.receiver.StopwatchActionReceiver
|
import de.jeanlucmakiola.clockula.stopwatch.receiver.StopwatchActionReceiver
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@@ -37,14 +50,16 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the stopwatch run and its laps reactively, then renders [StopwatchWidgetContent].
|
* Reads this instance's style, the stopwatch run and its laps reactively,
|
||||||
* Collected *inside* the composition: `update()` on a still-live session (one
|
* then renders [StopwatchWidgetContent]. Collected *inside* the composition:
|
||||||
* lives ~45s) only recomposes, so a value read once before `provideContent`
|
* `update()` on a still-live session (one lives ~45s) only recomposes, so a
|
||||||
* would keep showing what was true when the session started — the
|
* value read once before `provideContent` would keep showing what was true
|
||||||
* `initial` value only makes the first frame right.
|
* when the session started — the `initial` value only makes the first frame
|
||||||
|
* right.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun StopwatchWidgetRoot(entryPoint: WidgetEntryPoint, initialRun: StopwatchRun, initialLapCount: Int) {
|
fun StopwatchWidgetRoot(entryPoint: WidgetEntryPoint, initialRun: StopwatchRun, initialLapCount: Int) {
|
||||||
|
val style = parseStopwatchWidgetStyle(currentState<Preferences>()[WIDGET_STOPWATCH_STYLE_KEY])
|
||||||
val repository = entryPoint.stopwatchRepository()
|
val repository = entryPoint.stopwatchRepository()
|
||||||
val run by remember { repository.run() }.collectAsState(initial = initialRun)
|
val run by remember { repository.run() }.collectAsState(initial = initialRun)
|
||||||
val lapCount by remember { repository.laps().map { it.size } }.collectAsState(initial = initialLapCount)
|
val lapCount by remember { repository.laps().map { it.size } }.collectAsState(initial = initialLapCount)
|
||||||
@@ -52,19 +67,34 @@ fun StopwatchWidgetRoot(entryPoint: WidgetEntryPoint, initialRun: StopwatchRun,
|
|||||||
// is exactly `now - elapsed`.
|
// is exactly `now - elapsed`.
|
||||||
val elapsedRealtime = remember(run, lapCount) { entryPoint.elapsedRealtimeClock().elapsedRealtime() }
|
val elapsedRealtime = remember(run, lapCount) { entryPoint.elapsedRealtimeClock().elapsedRealtime() }
|
||||||
val model = remember(run, lapCount) { StopwatchWidgetPresenter.present(run, elapsedRealtime, lapCount) }
|
val model = remember(run, lapCount) { StopwatchWidgetPresenter.present(run, elapsedRealtime, lapCount) }
|
||||||
StopwatchWidgetContent(model, elapsedRealtime.inWholeMilliseconds)
|
StopwatchWidgetContent(model, elapsedRealtime.inWholeMilliseconds, style)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The stopwatch widget's content, given an already-resolved [model]: the
|
* The stopwatch widget's content, given an already-resolved [model], in
|
||||||
* readout as large as the widget allows, a wide pill for start/pause and a
|
* [style]: [CardFace] or, for every other style, [ShapeFace]. No 1s ticker —
|
||||||
* round lap/reset beside it. Below [WIDE_BELOW_HEIGHT] the controls move
|
* a running readout is the platform chronometer, based at
|
||||||
* beside the readout instead of under it. No 1s ticker — a running readout
|
* [elapsedRealtimeNow] (the `SystemClock.elapsedRealtime()` millis the model
|
||||||
* is the platform chronometer, based at [elapsedRealtimeNow] (the
|
* was resolved at).
|
||||||
* `SystemClock.elapsedRealtime()` millis the model was resolved at).
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun StopwatchWidgetContent(model: StopwatchWidgetModel, elapsedRealtimeNow: Long) {
|
fun StopwatchWidgetContent(model: StopwatchWidgetModel, elapsedRealtimeNow: Long, style: StopwatchWidgetStyle) {
|
||||||
|
when (style) {
|
||||||
|
StopwatchWidgetStyle.CARD -> CardFace(model, elapsedRealtimeNow)
|
||||||
|
StopwatchWidgetStyle.CIRCLE -> ShapeFace(model, elapsedRealtimeNow, R.drawable.widget_bg_circle, ROUND_INSET)
|
||||||
|
StopwatchWidgetStyle.COOKIE -> ShapeFace(model, elapsedRealtimeNow, R.drawable.widget_shape_cookie, ROUND_INSET)
|
||||||
|
StopwatchWidgetStyle.SUNNY -> ShapeFace(model, elapsedRealtimeNow, R.drawable.widget_shape_sunny, ROUND_INSET)
|
||||||
|
StopwatchWidgetStyle.SQUARE -> ShapeFace(model, elapsedRealtimeNow, R.drawable.widget_shape_square, SQUARE_INSET)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The card: the readout as large as the widget allows, a wide pill for
|
||||||
|
* start/pause and a round lap/reset beside it. Below [WIDE_BELOW_HEIGHT] the
|
||||||
|
* controls move beside the readout instead of under it.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun CardFace(model: StopwatchWidgetModel, elapsedRealtimeNow: Long) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val size = LocalSize.current
|
val size = LocalSize.current
|
||||||
val w = size.width.value
|
val w = size.width.value
|
||||||
@@ -118,12 +148,121 @@ fun StopwatchWidgetContent(model: StopwatchWidgetModel, elapsedRealtimeNow: Long
|
|||||||
/** Under this height the controls sit beside the readout. */
|
/** Under this height the controls sit beside the readout. */
|
||||||
private const val WIDE_BELOW_HEIGHT = 120f
|
private const val WIDE_BELOW_HEIGHT = 120f
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [shape] floating on the home screen, like the analog faces, holding the
|
||||||
|
* readout. Start/pause is a button of its own overlapping the shape's
|
||||||
|
* lower-left edge — a rounded square to start or resume that rounds into a
|
||||||
|
* circle while it runs — and, once the stopwatch has run, lap (while running)
|
||||||
|
* and reset share a tertiary pill at its upper right. Both are pulled in from
|
||||||
|
* the square's corners by [inset] (a fraction of its side), so they straddle
|
||||||
|
* the shape's edge rather than float clear of it. Everything is a fraction of
|
||||||
|
* the square the widget fits, so the face scales as one piece.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun Readout(context: Context, model: StopwatchWidgetModel, elapsedRealtimeNow: Long, sizeDp: Float) {
|
private fun ShapeFace(model: StopwatchWidgetModel, elapsedRealtimeNow: Long, @DrawableRes shape: Int, inset: Float) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val size = LocalSize.current
|
||||||
|
val side = minOf(size.width, size.height)
|
||||||
|
val s = side.value
|
||||||
|
val running = model.mode == StopwatchState.RUNNING
|
||||||
|
// The shape leaves the square's corners free, and the buttons sit there.
|
||||||
|
val body = s * 0.84f
|
||||||
|
val readoutChars = maxOf(5, ClockFormat.elapsed(model.elapsed).length)
|
||||||
|
// Fits the shape's inner ~62% — Sunny's and Cookie's lobes cut in deeper than a circle's edge.
|
||||||
|
val timeSize = minOf(s * 0.17f, body * 0.62f / (readoutChars * 0.6f))
|
||||||
|
val button = s * 0.28f
|
||||||
|
val action = s * 0.2f
|
||||||
|
|
||||||
|
CenteredSquare(side = side, onClick = actionStartActivity(showStopwatchIntent(context))) {
|
||||||
|
Box(modifier = GlanceModifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Column(
|
||||||
|
modifier = GlanceModifier
|
||||||
|
.size(body.dp)
|
||||||
|
.background(ImageProvider(shape), colorFilter = ColorFilter.tint(GlanceTheme.colors.primaryContainer)),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Readout(context, model, elapsedRealtimeNow, timeSize, onShape = true)
|
||||||
|
if (model.lapCount > 0) {
|
||||||
|
Text(
|
||||||
|
text = context.getString(R.string.widget_stopwatch_laps, model.lapCount),
|
||||||
|
style = TextStyle(
|
||||||
|
color = GlanceTheme.colors.onPrimaryContainer,
|
||||||
|
fontSize = fittedSp(context, (s * 0.065f).coerceAtLeast(10f)),
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
),
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(modifier = GlanceModifier.fillMaxSize().padding((s * inset).dp), contentAlignment = Alignment.BottomStart) {
|
||||||
|
ShapedIcon(
|
||||||
|
icon = if (running) R.drawable.widget_ic_pause else R.drawable.widget_ic_play,
|
||||||
|
contentDescription = context.getString(startPauseLabel(model.mode)),
|
||||||
|
width = button.dp,
|
||||||
|
height = button.dp,
|
||||||
|
container = GlanceTheme.colors.primary,
|
||||||
|
content = GlanceTheme.colors.onPrimary,
|
||||||
|
shape = if (running) R.drawable.widget_bg_circle else R.drawable.widget_shape_square,
|
||||||
|
iconFraction = 0.4f,
|
||||||
|
onClick = actionSendBroadcast(
|
||||||
|
actionIntent(context, if (running) StopwatchIntents.ACTION_PAUSE else StopwatchIntents.ACTION_RESUME),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (model.mode != StopwatchState.IDLE) {
|
||||||
|
Box(modifier = GlanceModifier.fillMaxSize().padding((s * inset).dp), contentAlignment = Alignment.TopEnd) {
|
||||||
|
Row(
|
||||||
|
modifier = GlanceModifier
|
||||||
|
.padding(horizontal = (action * 0.15f).dp)
|
||||||
|
.background(ImageProvider(R.drawable.widget_bg_pill), colorFilter = ColorFilter.tint(GlanceTheme.colors.tertiaryContainer)),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
if (running) {
|
||||||
|
PillAction(R.drawable.widget_ic_flag, context.getString(R.string.stopwatch_lap), action, StopwatchIntents.ACTION_LAP)
|
||||||
|
}
|
||||||
|
PillAction(R.drawable.widget_ic_replay, context.getString(R.string.stopwatch_reset), action, StopwatchIntents.ACTION_RESET)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [ShapeFace]'s control insets. A round shape's edge on the diagonal is ~0.2
|
||||||
|
* of the side in from the square's corner, so its controls come in further
|
||||||
|
* than the Square's, whose rounded corner nearly fills the square's own.
|
||||||
|
*/
|
||||||
|
private const val ROUND_INSET = 0.065f
|
||||||
|
private const val SQUARE_INSET = 0.025f
|
||||||
|
|
||||||
|
/** One of [ShapeFace]'s pill actions: a bare glyph on the pill's own colour, its whole [size] the touch target. */
|
||||||
|
@Composable
|
||||||
|
private fun PillAction(@DrawableRes icon: Int, description: String, size: Float, action: String) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
Box(
|
||||||
|
modifier = GlanceModifier
|
||||||
|
.size(size.dp)
|
||||||
|
.clickable(actionSendBroadcast(actionIntent(context, action))),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
provider = ImageProvider(icon),
|
||||||
|
contentDescription = description,
|
||||||
|
modifier = GlanceModifier.size((size * 0.5f).dp),
|
||||||
|
colorFilter = ColorFilter.tint(GlanceTheme.colors.onTertiaryContainer),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** [onShape]: the readout sits on [ShapeFace]'s primary-container shape rather than the card's surface. */
|
||||||
|
@Composable
|
||||||
|
private fun Readout(context: Context, model: StopwatchWidgetModel, elapsedRealtimeNow: Long, sizeDp: Float, onShape: Boolean = false) {
|
||||||
if (model.mode == StopwatchState.RUNNING) {
|
if (model.mode == StopwatchState.RUNNING) {
|
||||||
WidgetChronometer(
|
WidgetChronometer(
|
||||||
layoutRes = R.layout.widget_chronometer,
|
layoutRes = if (onShape) R.layout.widget_chronometer_on_primary else R.layout.widget_chronometer,
|
||||||
viewId = R.id.widget_chronometer,
|
viewId = if (onShape) R.id.widget_chronometer_on_primary else R.id.widget_chronometer,
|
||||||
base = elapsedRealtimeNow - model.elapsed.inWholeMilliseconds,
|
base = elapsedRealtimeNow - model.elapsed.inWholeMilliseconds,
|
||||||
countDown = false,
|
countDown = false,
|
||||||
textSizeDp = sizeDp,
|
textSizeDp = sizeDp,
|
||||||
@@ -132,11 +271,21 @@ private fun Readout(context: Context, model: StopwatchWidgetModel, elapsedRealti
|
|||||||
WidgetReadout(
|
WidgetReadout(
|
||||||
text = ClockFormat.elapsed(model.elapsed),
|
text = ClockFormat.elapsed(model.elapsed),
|
||||||
textSizeDp = sizeDp,
|
textSizeDp = sizeDp,
|
||||||
colorRes = if (model.mode == StopwatchState.IDLE) R.color.widget_on_surface_variant else R.color.widget_on_surface,
|
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
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startPauseLabel(mode: StopwatchState): Int = when (mode) {
|
||||||
|
StopwatchState.IDLE -> R.string.stopwatch_start
|
||||||
|
StopwatchState.RUNNING -> R.string.stopwatch_pause
|
||||||
|
StopwatchState.PAUSED -> R.string.stopwatch_resume
|
||||||
|
}
|
||||||
|
|
||||||
/** A wide primary pill (start/pause) and, once the stopwatch has run, a round lap/reset beside it. */
|
/** A wide primary pill (start/pause) and, once the stopwatch has run, a round lap/reset beside it. */
|
||||||
@Composable
|
@Composable
|
||||||
private fun Controls(context: Context, model: StopwatchWidgetModel, button: Float) {
|
private fun Controls(context: Context, model: StopwatchWidgetModel, button: Float) {
|
||||||
@@ -144,13 +293,7 @@ private fun Controls(context: Context, model: StopwatchWidgetModel, button: Floa
|
|||||||
val running = model.mode == StopwatchState.RUNNING
|
val running = model.mode == StopwatchState.RUNNING
|
||||||
ShapedIcon(
|
ShapedIcon(
|
||||||
icon = if (running) R.drawable.widget_ic_pause else R.drawable.widget_ic_play,
|
icon = if (running) R.drawable.widget_ic_pause else R.drawable.widget_ic_play,
|
||||||
contentDescription = context.getString(
|
contentDescription = context.getString(startPauseLabel(model.mode)),
|
||||||
when (model.mode) {
|
|
||||||
StopwatchState.IDLE -> R.string.stopwatch_start
|
|
||||||
StopwatchState.RUNNING -> R.string.stopwatch_pause
|
|
||||||
StopwatchState.PAUSED -> R.string.stopwatch_resume
|
|
||||||
},
|
|
||||||
),
|
|
||||||
width = (button * 1.6f).dp,
|
width = (button * 1.6f).dp,
|
||||||
height = button.dp,
|
height = button.dp,
|
||||||
container = GlanceTheme.colors.primary,
|
container = GlanceTheme.colors.primary,
|
||||||
|
|||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
package de.jeanlucmakiola.clockula.ui.widget.config
|
||||||
|
|
||||||
|
import android.os.SystemClock
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.RemoteViews
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
|
||||||
|
import androidx.glance.appwidget.GlanceRemoteViews
|
||||||
|
import androidx.glance.appwidget.state.getAppWidgetState
|
||||||
|
import androidx.glance.appwidget.state.updateAppWidgetState
|
||||||
|
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
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.parseStopwatchWidgetStyle
|
||||||
|
import de.jeanlucmakiola.clockula.ui.widget.StopwatchWidgetContent
|
||||||
|
import de.jeanlucmakiola.clockula.widget.StopwatchWidget
|
||||||
|
import de.jeanlucmakiola.clockula.widget.WIDGET_STOPWATCH_STYLE_KEY
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The stopwatch widget's entire configuration: a style pick. Like
|
||||||
|
* [AnalogClockConfigActivity], tapping an option saves and finishes
|
||||||
|
* immediately.
|
||||||
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class StopwatchConfigActivity : WidgetConfigActivity() {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
var style by remember { mutableStateOf(StopwatchWidgetStyle.CARD) }
|
||||||
|
LaunchedEffect(glanceId) {
|
||||||
|
val state = getAppWidgetState(this@StopwatchConfigActivity, PreferencesGlanceStateDefinition, glanceId)
|
||||||
|
style = parseStopwatchWidgetStyle(state[WIDGET_STOPWATCH_STYLE_KEY])
|
||||||
|
}
|
||||||
|
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
|
||||||
|
save {
|
||||||
|
updateAppWidgetState(this@StopwatchConfigActivity, glanceId) { prefs ->
|
||||||
|
prefs[WIDGET_STOPWATCH_STYLE_KEY] = picked.name
|
||||||
|
}
|
||||||
|
StopwatchWidget().update(this@StopwatchConfigActivity, glanceId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDismiss = ::dismiss,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun stopwatchStyleLabel(style: StopwatchWidgetStyle): Int = when (style) {
|
||||||
|
StopwatchWidgetStyle.CARD -> R.string.stopwatch_style_card
|
||||||
|
StopwatchWidgetStyle.CIRCLE -> R.string.stopwatch_style_circle
|
||||||
|
StopwatchWidgetStyle.COOKIE -> R.string.analog_style_cookie
|
||||||
|
StopwatchWidgetStyle.SUNNY -> R.string.analog_style_sunny
|
||||||
|
StopwatchWidgetStyle.SQUARE -> R.string.analog_style_square
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The real widget face, not a redrawing: [StopwatchWidgetContent] composed to
|
||||||
|
* `RemoteViews` by Glance itself and inflated in-app, showing a running
|
||||||
|
* stopwatch with laps banked — every control the face has. The chronometer
|
||||||
|
* ticks as it would on the home screen.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun StopwatchStylePreview(style: StopwatchWidgetStyle) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val size = if (style == StopwatchWidgetStyle.CARD) CardPreviewSize else ShapePreviewSize
|
||||||
|
val views by produceState<RemoteViews?>(initialValue = null, style) {
|
||||||
|
val now = SystemClock.elapsedRealtime()
|
||||||
|
value = GlanceRemoteViews().compose(context, size) {
|
||||||
|
StopwatchWidgetContent(PreviewModel, now, style)
|
||||||
|
}.remoteViews
|
||||||
|
}
|
||||||
|
AndroidView(
|
||||||
|
// Unlike the analog face, this one carries the widget's own click
|
||||||
|
// actions (open the app, start, lap, reset); it must not take touches,
|
||||||
|
// or a tap fires them instead of picking the style.
|
||||||
|
factory = { ctx ->
|
||||||
|
object : FrameLayout(ctx) {
|
||||||
|
override fun dispatchTouchEvent(event: MotionEvent): Boolean = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update = { frame ->
|
||||||
|
frame.removeAllViews()
|
||||||
|
views?.let { frame.addView(it.apply(frame.context, frame)) }
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(size),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val PreviewModel = StopwatchWidgetModel(mode = StopwatchState.RUNNING, elapsed = 83.seconds, lapCount = 2)
|
||||||
|
private val CardPreviewSize = DpSize(240.dp, 150.dp)
|
||||||
|
private val ShapePreviewSize = DpSize(180.dp, 180.dp)
|
||||||
@@ -20,6 +20,9 @@ internal val WIDGET_SHOW_WORLD_CLOCKS_KEY = booleanPreferencesKey("show_world_cl
|
|||||||
/** Per-instance Glance state key holding the analog widget's [AnalogClockStyle] ([name]). */
|
/** Per-instance Glance state key holding the analog widget's [AnalogClockStyle] ([name]). */
|
||||||
internal val WIDGET_ANALOG_STYLE_KEY = stringPreferencesKey("analog_style")
|
internal val WIDGET_ANALOG_STYLE_KEY = stringPreferencesKey("analog_style")
|
||||||
|
|
||||||
|
/** Per-instance Glance state key holding the stopwatch widget's `StopwatchWidgetStyle` ([name]). */
|
||||||
|
internal val WIDGET_STOPWATCH_STYLE_KEY = stringPreferencesKey("stopwatch_style")
|
||||||
|
|
||||||
/** Per-instance Glance state key holding the quick-timer widget's configured duration, in millis. */
|
/** Per-instance Glance state key holding the quick-timer widget's configured duration, in millis. */
|
||||||
internal val WIDGET_QUICK_TIMER_DURATION_KEY = longPreferencesKey("quick_timer_duration_millis")
|
internal val WIDGET_QUICK_TIMER_DURATION_KEY = longPreferencesKey("quick_timer_duration_millis")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- `MaterialShapes.Sunny` (`values/widget_shapes.xml`), white so its user
|
||||||
|
tints it — see `widget_bg_card.xml`. -->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="48dp"
|
||||||
|
android:height="48dp"
|
||||||
|
android:viewportWidth="100"
|
||||||
|
android:viewportHeight="100">
|
||||||
|
<path
|
||||||
|
android:pathData="@string/widget_shape_path_sunny"
|
||||||
|
android:fillColor="#FFFFFFFF" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- The widget picker's preview of the next-alarm widget (`previewLayout`,
|
||||||
|
API 31+): a static stand-in for `ui/widget/NextAlarmWidgetContent.kt` —
|
||||||
|
the Cookie alarm badge, a day, a time and a label on the surface card. -->
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@drawable/widget_bg_card"
|
||||||
|
android:backgroundTint="@color/widget_surface"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="14dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="52dp"
|
||||||
|
android:layout_height="52dp"
|
||||||
|
android:background="@drawable/widget_shape_cookie"
|
||||||
|
android:backgroundTint="@color/widget_primary_container"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:padding="13dp"
|
||||||
|
android:src="@drawable/widget_ic_alarm"
|
||||||
|
android:tint="@color/widget_on_primary_container" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="@string/widget_next_alarm_today"
|
||||||
|
android:textColor="@color/widget_primary"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="@string/widget_preview_alarm_time"
|
||||||
|
android:textColor="@color/widget_on_surface"
|
||||||
|
android:textSize="30sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- The widget picker's preview of the quick-timer widget (`previewLayout`,
|
||||||
|
API 31+): a static stand-in for `ui/widget/QuickTimerWidgetContent.kt`
|
||||||
|
at rest — the Scallop with a play glyph over its default 05:00. -->
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:src="@drawable/widget_shape_scallop"
|
||||||
|
android:tint="@color/widget_primary_container" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="18dp"
|
||||||
|
android:layout_height="18dp"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:src="@drawable/widget_ic_play"
|
||||||
|
android:tint="@color/widget_on_primary_container" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="@string/widget_preview_quick_timer_time"
|
||||||
|
android:textColor="@color/widget_on_primary_container"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- The widget picker's preview of the stopwatch widget (`previewLayout`,
|
||||||
|
API 31+): a static stand-in for `ui/widget/StopwatchWidgetContent.kt`'s
|
||||||
|
Circle face, running — the shape with its readout, the circular pause
|
||||||
|
against its lower-left edge, lap and reset in the pill at its upper
|
||||||
|
right. Fixed at a size that fits two cells. -->
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="112dp"
|
||||||
|
android:layout_height="112dp"
|
||||||
|
android:layout_gravity="center">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="94dp"
|
||||||
|
android:layout_height="94dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:background="@drawable/widget_bg_circle"
|
||||||
|
android:backgroundTint="@color/widget_primary_container"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:gravity="center"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="@string/widget_preview_stopwatch_time"
|
||||||
|
android:textColor="@color/widget_on_primary_container"
|
||||||
|
android:textSize="19sp" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="31dp"
|
||||||
|
android:layout_height="31dp"
|
||||||
|
android:layout_gravity="bottom|start"
|
||||||
|
android:layout_margin="7dp"
|
||||||
|
android:background="@drawable/widget_bg_circle"
|
||||||
|
android:backgroundTint="@color/widget_primary"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:padding="9dp"
|
||||||
|
android:src="@drawable/widget_ic_pause"
|
||||||
|
android:tint="@color/widget_on_primary" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="22dp"
|
||||||
|
android:layout_gravity="top|end"
|
||||||
|
android:layout_margin="7dp"
|
||||||
|
android:background="@drawable/widget_bg_pill"
|
||||||
|
android:backgroundTint="@color/widget_tertiary_container"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="4dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="22dp"
|
||||||
|
android:layout_height="22dp"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:padding="5dp"
|
||||||
|
android:src="@drawable/widget_ic_flag"
|
||||||
|
android:tint="@color/widget_on_tertiary_container" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="22dp"
|
||||||
|
android:layout_height="22dp"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:padding="5dp"
|
||||||
|
android:src="@drawable/widget_ic_replay"
|
||||||
|
android:tint="@color/widget_on_tertiary_container" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- The widget picker's preview of the timer widget (`previewLayout`,
|
||||||
|
API 31+): a static stand-in for `ui/widget/TimerWidgetContent.kt`'s lone
|
||||||
|
running timer — readout over a circular pause and a reset. -->
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@drawable/widget_bg_card"
|
||||||
|
android:backgroundTint="@color/widget_surface"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="14dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="@string/widget_preview_timer_time"
|
||||||
|
android:textColor="@color/widget_on_surface"
|
||||||
|
android:textSize="40sp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="44dp"
|
||||||
|
android:layout_height="44dp"
|
||||||
|
android:background="@drawable/widget_bg_circle"
|
||||||
|
android:backgroundTint="@color/widget_primary"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:padding="11dp"
|
||||||
|
android:src="@drawable/widget_ic_pause"
|
||||||
|
android:tint="@color/widget_on_primary" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="44dp"
|
||||||
|
android:layout_height="44dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:background="@drawable/widget_bg_circle"
|
||||||
|
android:backgroundTint="@color/widget_secondary_container"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:padding="11dp"
|
||||||
|
android:src="@drawable/widget_ic_replay"
|
||||||
|
android:tint="@color/widget_on_secondary_container" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -18,6 +18,9 @@
|
|||||||
<color name="widget_primary">@android:color/system_accent1_200</color>
|
<color name="widget_primary">@android:color/system_accent1_200</color>
|
||||||
<color name="widget_primary_container">@android:color/system_accent1_700</color>
|
<color name="widget_primary_container">@android:color/system_accent1_700</color>
|
||||||
<color name="widget_on_primary_container">@android:color/system_accent1_100</color>
|
<color name="widget_on_primary_container">@android:color/system_accent1_100</color>
|
||||||
|
<color name="widget_on_primary">@android:color/system_accent1_800</color>
|
||||||
|
<color name="widget_secondary_container">@android:color/system_accent2_700</color>
|
||||||
|
<color name="widget_on_secondary_container">@android:color/system_accent2_100</color>
|
||||||
<color name="widget_tertiary">@android:color/system_accent3_200</color>
|
<color name="widget_tertiary">@android:color/system_accent3_200</color>
|
||||||
<color name="widget_tertiary_container">@android:color/system_accent3_700</color>
|
<color name="widget_tertiary_container">@android:color/system_accent3_700</color>
|
||||||
<color name="widget_on_tertiary_container">@android:color/system_accent3_100</color>
|
<color name="widget_on_tertiary_container">@android:color/system_accent3_100</color>
|
||||||
|
|||||||
@@ -20,6 +20,9 @@
|
|||||||
<color name="widget_primary">#FFCEE2BA</color>
|
<color name="widget_primary">#FFCEE2BA</color>
|
||||||
<color name="widget_primary_container">#FF4A583B</color>
|
<color name="widget_primary_container">#FF4A583B</color>
|
||||||
<color name="widget_on_primary_container">#FFE8F6D9</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">#FFA5CEC7</color>
|
||||||
<color name="widget_tertiary_container">#FF244D48</color>
|
<color name="widget_tertiary_container">#FF244D48</color>
|
||||||
<color name="widget_on_tertiary_container">#FFC1ECE4</color>
|
<color name="widget_on_tertiary_container">#FFC1ECE4</color>
|
||||||
|
|||||||
@@ -22,6 +22,9 @@
|
|||||||
<color name="widget_primary">@android:color/system_accent1_600</color>
|
<color name="widget_primary">@android:color/system_accent1_600</color>
|
||||||
<color name="widget_primary_container">@android:color/system_accent1_100</color>
|
<color name="widget_primary_container">@android:color/system_accent1_100</color>
|
||||||
<color name="widget_on_primary_container">@android:color/system_accent1_900</color>
|
<color name="widget_on_primary_container">@android:color/system_accent1_900</color>
|
||||||
|
<color name="widget_on_primary">@android:color/system_accent1_0</color>
|
||||||
|
<color name="widget_secondary_container">@android:color/system_accent2_100</color>
|
||||||
|
<color name="widget_on_secondary_container">@android:color/system_accent2_900</color>
|
||||||
<color name="widget_tertiary">@android:color/system_accent3_600</color>
|
<color name="widget_tertiary">@android:color/system_accent3_600</color>
|
||||||
<color name="widget_tertiary_container">@android:color/system_accent3_100</color>
|
<color name="widget_tertiary_container">@android:color/system_accent3_100</color>
|
||||||
<color name="widget_on_tertiary_container">@android:color/system_accent3_900</color>
|
<color name="widget_on_tertiary_container">@android:color/system_accent3_900</color>
|
||||||
|
|||||||
@@ -34,6 +34,9 @@
|
|||||||
<color name="widget_primary">#FF5C6A4E</color>
|
<color name="widget_primary">#FF5C6A4E</color>
|
||||||
<color name="widget_primary_container">#FFD7E8C6</color>
|
<color name="widget_primary_container">#FFD7E8C6</color>
|
||||||
<color name="widget_on_primary_container">#FF1D2A10</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">#FF3D6660</color>
|
||||||
<color name="widget_tertiary_container">#FFCCE8E3</color>
|
<color name="widget_tertiary_container">#FFCCE8E3</color>
|
||||||
<color name="widget_on_tertiary_container">#FF05201C</color>
|
<color name="widget_on_tertiary_container">#FF05201C</color>
|
||||||
|
|||||||
@@ -358,13 +358,20 @@
|
|||||||
<string name="selfcheck_next_alarm_problem">Doesn\'t match what Clockula scheduled — open the app to re-check</string>
|
<string name="selfcheck_next_alarm_problem">Doesn\'t match what Clockula scheduled — open the app to re-check</string>
|
||||||
|
|
||||||
<!-- M10: widgets -->
|
<!-- M10: widgets -->
|
||||||
<string name="widget_next_alarm_description">Next alarm</string>
|
<!-- Sample values for the widget picker's previews (`layout/widget_preview_*.xml`). -->
|
||||||
|
<string name="widget_preview_alarm_time" translatable="false">7:00</string>
|
||||||
|
<string name="widget_preview_timer_time" translatable="false">04:59</string>
|
||||||
|
<string name="widget_preview_stopwatch_time" translatable="false">01:23</string>
|
||||||
|
<string name="widget_preview_quick_timer_time" translatable="false">05:00</string>
|
||||||
|
<string name="widget_next_alarm_label">Next alarm</string>
|
||||||
<string name="widget_next_alarm_empty">No alarms set</string>
|
<string name="widget_next_alarm_empty">No alarms set</string>
|
||||||
<string name="widget_next_alarm_today">Today</string>
|
<string name="widget_next_alarm_today">Today</string>
|
||||||
<string name="widget_timer_description">Timer</string>
|
<string name="widget_timer_label">Timer</string>
|
||||||
|
<string name="widget_timer_description">Your running timers</string>
|
||||||
<string name="widget_timer_new">New timer</string>
|
<string name="widget_timer_new">New timer</string>
|
||||||
<string name="widget_timer_expired">Done</string>
|
<string name="widget_timer_expired">Done</string>
|
||||||
<string name="widget_clock_description">Clock</string>
|
<string name="widget_clock_label">Digital clock</string>
|
||||||
|
<string name="widget_clock_description">With date and world clocks</string>
|
||||||
|
|
||||||
<!-- M11: digital clock widget faces + per-instance config -->
|
<!-- M11: digital clock widget faces + per-instance config -->
|
||||||
<string name="widget_config_title">Digital clock</string>
|
<string name="widget_config_title">Digital clock</string>
|
||||||
@@ -379,7 +386,7 @@
|
|||||||
<string name="widget_config_show_world_clocks_hint">Up to three cities from your World clock tab</string>
|
<string name="widget_config_show_world_clocks_hint">Up to three cities from your World clock tab</string>
|
||||||
|
|
||||||
<!-- M11: analog clock widget -->
|
<!-- M11: analog clock widget -->
|
||||||
<string name="widget_analog_description">Analog clock</string>
|
<string name="widget_analog_label">Analog clock</string>
|
||||||
<string name="widget_analog_config_title">Analog clock</string>
|
<string name="widget_analog_config_title">Analog clock</string>
|
||||||
<string name="analog_style_round">Round</string>
|
<string name="analog_style_round">Round</string>
|
||||||
<string name="analog_style_numeral">Numeral</string>
|
<string name="analog_style_numeral">Numeral</string>
|
||||||
@@ -392,11 +399,15 @@
|
|||||||
<string name="analog_style_scallop">Scallop</string>
|
<string name="analog_style_scallop">Scallop</string>
|
||||||
|
|
||||||
<!-- M11: stopwatch widget -->
|
<!-- M11: stopwatch widget -->
|
||||||
<string name="widget_stopwatch_description">Stopwatch</string>
|
<string name="widget_stopwatch_label">Stopwatch</string>
|
||||||
<string name="widget_stopwatch_laps">Laps: %1$d</string>
|
<string name="widget_stopwatch_laps">Laps: %1$d</string>
|
||||||
|
<string name="widget_stopwatch_config_title">Stopwatch</string>
|
||||||
|
<string name="stopwatch_style_card">Card</string>
|
||||||
|
<string name="stopwatch_style_circle">Circle</string>
|
||||||
|
|
||||||
<!-- M11: quick-timer widget -->
|
<!-- M11: quick-timer widget -->
|
||||||
<string name="widget_quick_timer_description">Quick timer</string>
|
<string name="widget_quick_timer_label">Quick timer</string>
|
||||||
|
<string name="widget_quick_timer_description">Starts a timer with one tap</string>
|
||||||
<string name="widget_quick_timer_config_title">Quick timer</string>
|
<string name="widget_quick_timer_config_title">Quick timer</string>
|
||||||
<string name="widget_quick_timer_start">Start %1$s</string>
|
<string name="widget_quick_timer_start">Start %1$s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
android:updatePeriodMillis="0"
|
android:updatePeriodMillis="0"
|
||||||
android:resizeMode="horizontal|vertical"
|
android:resizeMode="horizontal|vertical"
|
||||||
android:widgetCategory="home_screen"
|
android:widgetCategory="home_screen"
|
||||||
android:description="@string/widget_analog_description"
|
|
||||||
android:configure="de.jeanlucmakiola.clockula.ui.widget.config.AnalogClockConfigActivity"
|
android:configure="de.jeanlucmakiola.clockula.ui.widget.config.AnalogClockConfigActivity"
|
||||||
android:widgetFeatures="reconfigurable|configuration_optional"
|
android:widgetFeatures="reconfigurable"
|
||||||
android:previewLayout="@layout/widget_analog_plain" />
|
android:previewLayout="@layout/widget_analog_plain" />
|
||||||
|
|||||||
@@ -10,4 +10,4 @@
|
|||||||
android:resizeMode="horizontal|vertical"
|
android:resizeMode="horizontal|vertical"
|
||||||
android:widgetCategory="home_screen"
|
android:widgetCategory="home_screen"
|
||||||
android:previewImage="@mipmap/ic_launcher"
|
android:previewImage="@mipmap/ic_launcher"
|
||||||
android:description="@string/widget_next_alarm_description" />
|
android:previewLayout="@layout/widget_preview_next_alarm" />
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
android:resizeMode="horizontal|vertical"
|
android:resizeMode="horizontal|vertical"
|
||||||
android:widgetCategory="home_screen"
|
android:widgetCategory="home_screen"
|
||||||
android:previewImage="@mipmap/ic_launcher"
|
android:previewImage="@mipmap/ic_launcher"
|
||||||
|
android:previewLayout="@layout/widget_preview_quick_timer"
|
||||||
android:description="@string/widget_quick_timer_description"
|
android:description="@string/widget_quick_timer_description"
|
||||||
android:configure="de.jeanlucmakiola.clockula.ui.widget.config.QuickTimerConfigActivity"
|
android:configure="de.jeanlucmakiola.clockula.ui.widget.config.QuickTimerConfigActivity"
|
||||||
android:widgetFeatures="reconfigurable|configuration_optional" />
|
android:widgetFeatures="reconfigurable|configuration_optional" />
|
||||||
|
|||||||
@@ -10,4 +10,6 @@
|
|||||||
android:resizeMode="horizontal|vertical"
|
android:resizeMode="horizontal|vertical"
|
||||||
android:widgetCategory="home_screen"
|
android:widgetCategory="home_screen"
|
||||||
android:previewImage="@mipmap/ic_launcher"
|
android:previewImage="@mipmap/ic_launcher"
|
||||||
android:description="@string/widget_stopwatch_description" />
|
android:previewLayout="@layout/widget_preview_stopwatch"
|
||||||
|
android:configure="de.jeanlucmakiola.clockula.ui.widget.config.StopwatchConfigActivity"
|
||||||
|
android:widgetFeatures="reconfigurable" />
|
||||||
|
|||||||
@@ -10,4 +10,5 @@
|
|||||||
android:resizeMode="horizontal|vertical"
|
android:resizeMode="horizontal|vertical"
|
||||||
android:widgetCategory="home_screen"
|
android:widgetCategory="home_screen"
|
||||||
android:previewImage="@mipmap/ic_launcher"
|
android:previewImage="@mipmap/ic_launcher"
|
||||||
|
android:previewLayout="@layout/widget_preview_timer"
|
||||||
android:description="@string/widget_timer_description" />
|
android:description="@string/widget_timer_description" />
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class ManifestRulesTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `exactly the launcher, the door, the three widget configurators and the six widget receivers are exported`() {
|
fun `exactly the launcher, the door, the four widget configurators and the six widget receivers are exported`() {
|
||||||
val exported = Regex("<(activity|service|receiver|provider)\\b[\\s\\S]*?(?:/>|</\\1>)")
|
val exported = Regex("<(activity|service|receiver|provider)\\b[\\s\\S]*?(?:/>|</\\1>)")
|
||||||
.findAll(text)
|
.findAll(text)
|
||||||
.map { it.value }
|
.map { it.value }
|
||||||
@@ -43,15 +43,16 @@ class ManifestRulesTest {
|
|||||||
// it as a widget provider at all, and `the six widget receivers
|
// it as a widget provider at all, and `the six widget receivers
|
||||||
// answer only APPWIDGET_UPDATE` pins that each one is guarded to
|
// answer only APPWIDGET_UPDATE` pins that each one is guarded to
|
||||||
// that one platform-delivered broadcast and nothing wider. M11 adds
|
// that one platform-delivered broadcast and nothing wider. M11 adds
|
||||||
// three widget config activities: exported for the same reason —
|
// four widget config activities: exported for the same reason —
|
||||||
// only the launcher can start an `APPWIDGET_CONFIGURE` activity
|
// only the launcher can start an `APPWIDGET_CONFIGURE` activity
|
||||||
// cross-package — and guarded to that one action by `the three
|
// cross-package — and guarded to that one action by `the four
|
||||||
// widget configurators answer only APPWIDGET_CONFIGURE` below.
|
// widget configurators answer only APPWIDGET_CONFIGURE` below.
|
||||||
assertThat(exported).containsExactly(
|
assertThat(exported).containsExactly(
|
||||||
".MainActivity",
|
".MainActivity",
|
||||||
".interop.AlarmClockActivity",
|
".interop.AlarmClockActivity",
|
||||||
".ui.widget.config.ClockWidgetConfigActivity",
|
".ui.widget.config.ClockWidgetConfigActivity",
|
||||||
".ui.widget.config.AnalogClockConfigActivity",
|
".ui.widget.config.AnalogClockConfigActivity",
|
||||||
|
".ui.widget.config.StopwatchConfigActivity",
|
||||||
".ui.widget.config.QuickTimerConfigActivity",
|
".ui.widget.config.QuickTimerConfigActivity",
|
||||||
".widget.NextAlarmWidgetReceiver",
|
".widget.NextAlarmWidgetReceiver",
|
||||||
".widget.TimerWidgetReceiver",
|
".widget.TimerWidgetReceiver",
|
||||||
@@ -64,14 +65,15 @@ class ManifestRulesTest {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* M11: the mirror of `the six widget receivers answer only
|
* M11: the mirror of `the six widget receivers answer only
|
||||||
* APPWIDGET_UPDATE` for the three widget config activities — being
|
* APPWIDGET_UPDATE` for the four widget config activities — being
|
||||||
* exported cannot be widened into a second, wider surface by accident.
|
* exported cannot be widened into a second, wider surface by accident.
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun `the three widget configurators answer only APPWIDGET_CONFIGURE`() {
|
fun `the four widget configurators answer only APPWIDGET_CONFIGURE`() {
|
||||||
val configurators = listOf(
|
val configurators = listOf(
|
||||||
".ui.widget.config.ClockWidgetConfigActivity",
|
".ui.widget.config.ClockWidgetConfigActivity",
|
||||||
".ui.widget.config.AnalogClockConfigActivity",
|
".ui.widget.config.AnalogClockConfigActivity",
|
||||||
|
".ui.widget.config.StopwatchConfigActivity",
|
||||||
".ui.widget.config.QuickTimerConfigActivity",
|
".ui.widget.config.QuickTimerConfigActivity",
|
||||||
)
|
)
|
||||||
for (name in configurators) {
|
for (name in configurators) {
|
||||||
|
|||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package de.jeanlucmakiola.clockula.domain.widget
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class StopwatchWidgetSettingsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `null parses to Card, so instances placed before styles existed keep their face`() {
|
||||||
|
assertThat(parseStopwatchWidgetStyle(null)).isEqualTo(StopwatchWidgetStyle.CARD)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `an unknown name parses to Card`() {
|
||||||
|
assertThat(parseStopwatchWidgetStyle("Nonexistent")).isEqualTo(StopwatchWidgetStyle.CARD)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `each stopwatch style's own name round-trips`() {
|
||||||
|
StopwatchWidgetStyle.entries.forEach { style ->
|
||||||
|
assertThat(parseStopwatchWidgetStyle(style.name)).isEqualTo(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user