feat(widgets): open Alarms from the clock widgets, or a tab of your choice
Tapping the digital or analog clock widget now opens Alarms instead of World clock, and both config screens gain an "On tap" setting (Alarms, Timers, Stopwatch, World clock) stored per instance next to the style. Instances placed before the setting existed open Alarms. The analog config becomes a settings screen like the digital one, with the style behind a row, to make room for it. A widget tap sometimes showed whatever the app was last on instead of its tab, for two reasons. The shell replayed a deep link as a tab tap with restoreState, so it restored the tab's saved back stack, nested screen and all (fixed by the pager shell, which pops to the tab). And the widget intents carried only NEW_TASK, so with a picker or settings page left on top of MainActivity the task was merely brought forward and onNewIntent never ran. Every widget that opens the app now builds its intent in one place with CLEAR_TOP and SINGLE_TOP as well. Closes #8
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
package de.jeanlucmakiola.clockula.domain.widget
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Which tab a clock widget — digital or analog — opens when tapped, in tab
|
||||||
|
* order. Picked per instance in the widget's config screen and stored by
|
||||||
|
* [name] next to its style (`widget/ClockWidgetState.kt`).
|
||||||
|
*
|
||||||
|
* [ALARMS] is the default, so an instance placed before the setting existed,
|
||||||
|
* or with configuration skipped, opens Alarms: a clock on the home screen is
|
||||||
|
* most often tapped to set or check an alarm.
|
||||||
|
*/
|
||||||
|
enum class ClockWidgetTapTarget { ALARMS, TIMERS, STOPWATCH, WORLD_CLOCK }
|
||||||
|
|
||||||
|
/** Anything unreadable, or a name this enum no longer has, falls back to [ClockWidgetTapTarget.ALARMS]. */
|
||||||
|
fun parseClockWidgetTapTarget(raw: String?): ClockWidgetTapTarget =
|
||||||
|
raw?.let { runCatching { ClockWidgetTapTarget.valueOf(it) }.getOrNull() } ?: ClockWidgetTapTarget.ALARMS
|
||||||
@@ -46,9 +46,10 @@ object ShellNavigation {
|
|||||||
/**
|
/**
|
||||||
* Internal only, like [de.jeanlucmakiola.clockula.interop.InteropIntents]'
|
* Internal only, like [de.jeanlucmakiola.clockula.interop.InteropIntents]'
|
||||||
* actions: nothing outside the app sends this. It exists because the clock
|
* actions: nothing outside the app sends this. It exists because the clock
|
||||||
* widget (M10) needs to open the World clock tab the same way a
|
* widgets (M10) need to open the World clock tab the same way a
|
||||||
* notification opens its tab, and no `AlarmClock`-contract action covers a
|
* notification opens its tab, and no `AlarmClock`-contract action covers a
|
||||||
* world clock.
|
* world clock. Since #8 that is one of four tabs a clock widget can open,
|
||||||
|
* and Alarms is the default.
|
||||||
*/
|
*/
|
||||||
const val ACTION_SHOW_WORLD_CLOCK: String = "de.jeanlucmakiola.clockula.action.SHOW_WORLD_CLOCK"
|
const val ACTION_SHOW_WORLD_CLOCK: String = "de.jeanlucmakiola.clockula.action.SHOW_WORLD_CLOCK"
|
||||||
|
|
||||||
@@ -128,6 +129,17 @@ object ShellNavigation {
|
|||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The internal action that opens [destination] — [tabForAction]'s inverse,
|
||||||
|
* for a surface that lets the user pick the tab (the clock widgets, #8).
|
||||||
|
*/
|
||||||
|
fun showActionOf(destination: ClockulaDestination): String = when (destination) {
|
||||||
|
ClockulaDestination.ALARMS -> AlarmIntents.ACTION_SHOW_ALARMS
|
||||||
|
ClockulaDestination.TIMERS -> TimerIntents.ACTION_SHOW_TIMERS
|
||||||
|
ClockulaDestination.STOPWATCH -> StopwatchIntents.ACTION_SHOW_STOPWATCH
|
||||||
|
ClockulaDestination.WORLD_CLOCK -> ACTION_SHOW_WORLD_CLOCK
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The request an incoming *internal* intent makes of the shell. Null for an
|
* The request an incoming *internal* intent makes of the shell. Null for an
|
||||||
* action the shell does not answer; **degrades rather than throws** — a
|
* action the shell does not answer; **degrades rather than throws** — a
|
||||||
|
|||||||
@@ -27,14 +27,20 @@ import androidx.glance.layout.fillMaxSize
|
|||||||
import androidx.glance.layout.size
|
import androidx.glance.layout.size
|
||||||
import de.jeanlucmakiola.clockula.R
|
import de.jeanlucmakiola.clockula.R
|
||||||
import de.jeanlucmakiola.clockula.domain.widget.AnalogClockStyle
|
import de.jeanlucmakiola.clockula.domain.widget.AnalogClockStyle
|
||||||
|
import de.jeanlucmakiola.clockula.domain.widget.ClockWidgetTapTarget
|
||||||
import de.jeanlucmakiola.clockula.domain.widget.parseAnalogClockStyle
|
import de.jeanlucmakiola.clockula.domain.widget.parseAnalogClockStyle
|
||||||
|
import de.jeanlucmakiola.clockula.domain.widget.parseClockWidgetTapTarget
|
||||||
import de.jeanlucmakiola.clockula.widget.WIDGET_ANALOG_STYLE_KEY
|
import de.jeanlucmakiola.clockula.widget.WIDGET_ANALOG_STYLE_KEY
|
||||||
|
import de.jeanlucmakiola.clockula.widget.WIDGET_TAP_TARGET_KEY
|
||||||
|
|
||||||
/** Reads this instance's own [AnalogClockStyle] reactively, then renders [AnalogClockWidgetContent]. */
|
/** Reads this instance's own [AnalogClockStyle] and tap target reactively, then renders [AnalogClockWidgetContent]. */
|
||||||
@Composable
|
@Composable
|
||||||
fun AnalogClockWidgetRoot() {
|
fun AnalogClockWidgetRoot() {
|
||||||
val prefs = currentState<Preferences>()
|
val prefs = currentState<Preferences>()
|
||||||
AnalogClockWidgetContent(style = parseAnalogClockStyle(prefs[WIDGET_ANALOG_STYLE_KEY]))
|
AnalogClockWidgetContent(
|
||||||
|
style = parseAnalogClockStyle(prefs[WIDGET_ANALOG_STYLE_KEY]),
|
||||||
|
tapTarget = parseClockWidgetTapTarget(prefs[WIDGET_TAP_TARGET_KEY]),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,13 +48,13 @@ fun AnalogClockWidgetRoot() {
|
|||||||
* floating directly on the home screen, not on a card, the way a platform
|
* floating directly on the home screen, not on a card, the way a platform
|
||||||
* clock widget looks. Its colours are the analog palette resources
|
* 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
|
* (`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.
|
* API 31+, the wallpaper by itself. A tap opens [tapTarget]'s tab.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun AnalogClockWidgetContent(style: AnalogClockStyle) {
|
fun AnalogClockWidgetContent(style: AnalogClockStyle, tapTarget: ClockWidgetTapTarget) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
Box(
|
Box(
|
||||||
modifier = GlanceModifier.fillMaxSize().clickable(actionStartActivity(showWorldClockIntent(context))),
|
modifier = GlanceModifier.fillMaxSize().clickable(actionStartActivity(clockTapIntent(context, tapTarget))),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
AnalogFace(style)
|
AnalogFace(style)
|
||||||
|
|||||||
@@ -17,10 +17,13 @@ import androidx.glance.layout.fillMaxSize
|
|||||||
import de.jeanlucmakiola.clockula.data.worldclocks.WorldClockRepository
|
import de.jeanlucmakiola.clockula.data.worldclocks.WorldClockRepository
|
||||||
import de.jeanlucmakiola.clockula.data.zones.ZoneNames
|
import de.jeanlucmakiola.clockula.data.zones.ZoneNames
|
||||||
import de.jeanlucmakiola.clockula.domain.widget.ClockWidgetPresenter
|
import de.jeanlucmakiola.clockula.domain.widget.ClockWidgetPresenter
|
||||||
|
import de.jeanlucmakiola.clockula.domain.widget.ClockWidgetTapTarget
|
||||||
import de.jeanlucmakiola.clockula.domain.widget.DigitalClockSettings
|
import de.jeanlucmakiola.clockula.domain.widget.DigitalClockSettings
|
||||||
|
import de.jeanlucmakiola.clockula.domain.widget.parseClockWidgetTapTarget
|
||||||
import de.jeanlucmakiola.clockula.widget.WIDGET_DIGITAL_STYLE_KEY
|
import de.jeanlucmakiola.clockula.widget.WIDGET_DIGITAL_STYLE_KEY
|
||||||
import de.jeanlucmakiola.clockula.widget.WIDGET_SHOW_DATE_KEY
|
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_SHOW_WORLD_CLOCKS_KEY
|
||||||
|
import de.jeanlucmakiola.clockula.widget.WIDGET_TAP_TARGET_KEY
|
||||||
import de.jeanlucmakiola.clockula.widget.di.WidgetEntryPoint
|
import de.jeanlucmakiola.clockula.widget.di.WidgetEntryPoint
|
||||||
import de.jeanlucmakiola.clockula.widget.parseDigitalClockSettings
|
import de.jeanlucmakiola.clockula.widget.parseDigitalClockSettings
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -63,7 +66,11 @@ fun ClockWidgetRoot(entryPoint: WidgetEntryPoint, initialCities: List<ClockWidge
|
|||||||
val cities by remember { widgetCitiesFlow(entryPoint.worldClockRepository(), entryPoint.zoneNames()) }
|
val cities by remember { widgetCitiesFlow(entryPoint.worldClockRepository(), entryPoint.zoneNames()) }
|
||||||
.collectAsState(initial = initialCities)
|
.collectAsState(initial = initialCities)
|
||||||
|
|
||||||
ClockWidgetContent(cities = if (settings.showWorldClocks) cities else emptyList(), settings = settings)
|
ClockWidgetContent(
|
||||||
|
cities = if (settings.showWorldClocks) cities else emptyList(),
|
||||||
|
settings = settings,
|
||||||
|
tapTarget = parseClockWidgetTapTarget(prefs[WIDGET_TAP_TARGET_KEY]),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,13 +78,13 @@ fun ClockWidgetRoot(entryPoint: WidgetEntryPoint, initialCities: List<ClockWidge
|
|||||||
* the widget's live size. Like the analog widget, the face draws its own
|
* 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
|
* 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
|
* `widget_*` palette resources, so it follows the launcher's day/night and
|
||||||
* the wallpaper by itself.
|
* the wallpaper by itself. A tap opens [tapTarget]'s tab.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun ClockWidgetContent(cities: List<ClockWidgetCity>, settings: DigitalClockSettings) {
|
fun ClockWidgetContent(cities: List<ClockWidgetCity>, settings: DigitalClockSettings, tapTarget: ClockWidgetTapTarget) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val size = LocalSize.current
|
val size = LocalSize.current
|
||||||
Box(modifier = GlanceModifier.fillMaxSize().clickable(actionStartActivity(showWorldClockIntent(context)))) {
|
Box(modifier = GlanceModifier.fillMaxSize().clickable(actionStartActivity(clockTapIntent(context, tapTarget)))) {
|
||||||
AndroidRemoteViews(
|
AndroidRemoteViews(
|
||||||
remoteViews = digitalClockViews(context, settings, cities, size.width.value, size.height.value),
|
remoteViews = digitalClockViews(context, settings, cities, size.width.value, size.height.value),
|
||||||
modifier = GlanceModifier.fillMaxSize(),
|
modifier = GlanceModifier.fillMaxSize(),
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ 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
|
||||||
import androidx.glance.text.TextStyle
|
import androidx.glance.text.TextStyle
|
||||||
import de.jeanlucmakiola.clockula.MainActivity
|
|
||||||
import de.jeanlucmakiola.clockula.R
|
import de.jeanlucmakiola.clockula.R
|
||||||
import de.jeanlucmakiola.clockula.alarm.AlarmIntents
|
import de.jeanlucmakiola.clockula.alarm.AlarmIntents
|
||||||
import de.jeanlucmakiola.clockula.domain.widget.NextAlarmWidgetModel
|
import de.jeanlucmakiola.clockula.domain.widget.NextAlarmWidgetModel
|
||||||
@@ -126,10 +125,7 @@ fun NextAlarmWidgetContent(model: NextAlarmWidgetModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showAlarmsIntent(context: Context): Intent =
|
private fun showAlarmsIntent(context: Context): Intent = openAppIntent(context, AlarmIntents.ACTION_SHOW_ALARMS)
|
||||||
Intent(context, MainActivity::class.java)
|
|
||||||
.setAction(AlarmIntents.ACTION_SHOW_ALARMS)
|
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
|
|
||||||
private fun timeLabel(context: Context, model: NextAlarmWidgetModel): String {
|
private fun timeLabel(context: Context, model: NextAlarmWidgetModel): String {
|
||||||
val time = LocalTime.of(model.hour, model.minute)
|
val time = LocalTime.of(model.hour, model.minute)
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ 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
|
||||||
import androidx.glance.text.TextStyle
|
import androidx.glance.text.TextStyle
|
||||||
import de.jeanlucmakiola.clockula.MainActivity
|
|
||||||
import de.jeanlucmakiola.clockula.R
|
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
|
||||||
@@ -323,7 +322,4 @@ private fun Controls(context: Context, model: StopwatchWidgetModel, button: Floa
|
|||||||
private fun actionIntent(context: Context, action: String): Intent =
|
private fun actionIntent(context: Context, action: String): Intent =
|
||||||
Intent(context, StopwatchActionReceiver::class.java).setAction(action)
|
Intent(context, StopwatchActionReceiver::class.java).setAction(action)
|
||||||
|
|
||||||
private fun showStopwatchIntent(context: Context): Intent =
|
private fun showStopwatchIntent(context: Context): Intent = openAppIntent(context, StopwatchIntents.ACTION_SHOW_STOPWATCH)
|
||||||
Intent(context, MainActivity::class.java)
|
|
||||||
.setAction(StopwatchIntents.ACTION_SHOW_STOPWATCH)
|
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ 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
|
||||||
import androidx.glance.text.TextStyle
|
import androidx.glance.text.TextStyle
|
||||||
import de.jeanlucmakiola.clockula.MainActivity
|
|
||||||
import de.jeanlucmakiola.clockula.R
|
import de.jeanlucmakiola.clockula.R
|
||||||
import de.jeanlucmakiola.clockula.domain.format.ClockFormat
|
import de.jeanlucmakiola.clockula.domain.format.ClockFormat
|
||||||
import de.jeanlucmakiola.clockula.domain.timer.TimerMode
|
import de.jeanlucmakiola.clockula.domain.timer.TimerMode
|
||||||
@@ -265,7 +264,5 @@ internal fun timerActionIntent(context: Context, action: String, timerId: Long):
|
|||||||
|
|
||||||
private fun showTimersOrComposeIntent(context: Context, model: TimerWidgetModel): Intent {
|
private fun showTimersOrComposeIntent(context: Context, model: TimerWidgetModel): Intent {
|
||||||
val action = if (model.items.isEmpty()) InteropIntents.ACTION_COMPOSE_TIMER else TimerIntents.ACTION_SHOW_TIMERS
|
val action = if (model.items.isEmpty()) InteropIntents.ACTION_COMPOSE_TIMER else TimerIntents.ACTION_SHOW_TIMERS
|
||||||
return Intent(context, MainActivity::class.java)
|
return openAppIntent(context, action)
|
||||||
.setAction(action)
|
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ import androidx.glance.layout.wrapContentSize
|
|||||||
import androidx.glance.unit.ColorProvider
|
import androidx.glance.unit.ColorProvider
|
||||||
import de.jeanlucmakiola.clockula.MainActivity
|
import de.jeanlucmakiola.clockula.MainActivity
|
||||||
import de.jeanlucmakiola.clockula.R
|
import de.jeanlucmakiola.clockula.R
|
||||||
|
import de.jeanlucmakiola.clockula.domain.widget.ClockWidgetTapTarget
|
||||||
|
import de.jeanlucmakiola.clockula.ui.shell.ClockulaDestination
|
||||||
import de.jeanlucmakiola.clockula.ui.theme.ClockulaDarkFallback
|
import de.jeanlucmakiola.clockula.ui.theme.ClockulaDarkFallback
|
||||||
import de.jeanlucmakiola.clockula.ui.theme.ClockulaLightFallback
|
import de.jeanlucmakiola.clockula.ui.theme.ClockulaLightFallback
|
||||||
import de.jeanlucmakiola.clockula.ui.shell.ShellNavigation
|
import de.jeanlucmakiola.clockula.ui.shell.ShellNavigation
|
||||||
@@ -184,11 +186,37 @@ internal fun CenteredSquare(side: Dp, onClick: Action, content: @Composable () -
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Opens the app on World clock — where both clock widgets lead. */
|
/**
|
||||||
internal fun showWorldClockIntent(context: Context): Intent =
|
* Opens the app for [action] — one of the internal actions `MainActivity`
|
||||||
|
* answers — from a widget tap. Every widget that opens the app goes through
|
||||||
|
* here, so every one lands where it says, whatever the app was showing.
|
||||||
|
*
|
||||||
|
* `CLEAR_TOP` + `SINGLE_TOP`, not `NEW_TASK` alone: with `NEW_TASK` alone an
|
||||||
|
* existing task is only brought forward, and if anything sits on top of
|
||||||
|
* `MainActivity` in it — a ringtone or file picker the editor opened, a
|
||||||
|
* system settings page — the intent is never delivered (or stacks a second
|
||||||
|
* `MainActivity` on it), and the user is shown whatever they left open.
|
||||||
|
* These finish anything above `MainActivity` and hand the intent to the
|
||||||
|
* running instance's `onNewIntent`, where the shell pops any nested screen
|
||||||
|
* back to the asked-for tab (`ShellNavigation.onTabRequested`, #8).
|
||||||
|
*/
|
||||||
|
internal fun openAppIntent(context: Context, action: String): Intent =
|
||||||
Intent(context, MainActivity::class.java)
|
Intent(context, MainActivity::class.java)
|
||||||
.setAction(ShellNavigation.ACTION_SHOW_WORLD_CLOCK)
|
.setAction(action)
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
|
|
||||||
|
/** Opens the app on [target] — the tab a clock widget instance was set to open (#8). */
|
||||||
|
internal fun clockTapIntent(context: Context, target: ClockWidgetTapTarget): Intent =
|
||||||
|
openAppIntent(context, ShellNavigation.showActionOf(target.destination))
|
||||||
|
|
||||||
|
/** The shell tab [this] opens. */
|
||||||
|
internal val ClockWidgetTapTarget.destination: ClockulaDestination
|
||||||
|
get() = when (this) {
|
||||||
|
ClockWidgetTapTarget.ALARMS -> ClockulaDestination.ALARMS
|
||||||
|
ClockWidgetTapTarget.TIMERS -> ClockulaDestination.TIMERS
|
||||||
|
ClockWidgetTapTarget.STOPWATCH -> ClockulaDestination.STOPWATCH
|
||||||
|
ClockWidgetTapTarget.WORLD_CLOCK -> ClockulaDestination.WORLD_CLOCK
|
||||||
|
}
|
||||||
|
|
||||||
internal fun currentLocale(context: Context): Locale =
|
internal fun currentLocale(context: Context): Locale =
|
||||||
ConfigurationCompat.getLocales(context.resources.configuration).get(0) ?: Locale.getDefault()
|
ConfigurationCompat.getLocales(context.resources.configuration).get(0) ?: Locale.getDefault()
|
||||||
|
|||||||
+79
-13
@@ -1,13 +1,21 @@
|
|||||||
package de.jeanlucmakiola.clockula.ui.widget.config
|
package de.jeanlucmakiola.clockula.ui.widget.config
|
||||||
|
|
||||||
import android.widget.FrameLayout
|
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.foundation.layout.size
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -19,37 +27,49 @@ import androidx.glance.state.PreferencesGlanceStateDefinition
|
|||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import de.jeanlucmakiola.clockula.R
|
import de.jeanlucmakiola.clockula.R
|
||||||
import de.jeanlucmakiola.clockula.domain.widget.AnalogClockStyle
|
import de.jeanlucmakiola.clockula.domain.widget.AnalogClockStyle
|
||||||
|
import de.jeanlucmakiola.clockula.domain.widget.ClockWidgetTapTarget
|
||||||
import de.jeanlucmakiola.clockula.domain.widget.parseAnalogClockStyle
|
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.ui.widget.analogClockViews
|
||||||
import de.jeanlucmakiola.clockula.widget.AnalogClockWidget
|
import de.jeanlucmakiola.clockula.widget.AnalogClockWidget
|
||||||
import de.jeanlucmakiola.clockula.widget.WIDGET_ANALOG_STYLE_KEY
|
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.floret.components.GroupedRow
|
||||||
|
import de.jeanlucmakiola.floret.components.Position
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The analog widget's entire configuration: a style pick, nothing else — this
|
* The analog widget's configuration: its style and the tab a tap opens (#8).
|
||||||
* activity *is* [WidgetStylePicker]; tapping an option saves and finishes
|
* Laid out like the digital widget's — the live face above the settings, the
|
||||||
* straight away, as the reference picker does (no separate "Save" step).
|
* 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.
|
||||||
*/
|
*/
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class AnalogClockConfigActivity : WidgetConfigActivity() {
|
class AnalogClockConfigActivity : WidgetConfigActivity() {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
var style by remember { mutableStateOf(AnalogClockStyle.ROUND) }
|
// `null` until this instance's stored settings are read, as in the
|
||||||
|
// digital config: neither an early save nor a late load can write
|
||||||
|
// defaults over what the user has.
|
||||||
|
var style by remember { mutableStateOf<AnalogClockStyle?>(null) }
|
||||||
|
var tapTarget by remember { mutableStateOf(ClockWidgetTapTarget.ALARMS) }
|
||||||
LaunchedEffect(glanceId) {
|
LaunchedEffect(glanceId) {
|
||||||
val state = getAppWidgetState(this@AnalogClockConfigActivity, PreferencesGlanceStateDefinition, glanceId)
|
val state = getAppWidgetState(this@AnalogClockConfigActivity, PreferencesGlanceStateDefinition, glanceId)
|
||||||
|
tapTarget = parseClockWidgetTapTarget(state[WIDGET_TAP_TARGET_KEY])
|
||||||
style = parseAnalogClockStyle(state[WIDGET_ANALOG_STYLE_KEY])
|
style = parseAnalogClockStyle(state[WIDGET_ANALOG_STYLE_KEY])
|
||||||
}
|
}
|
||||||
WidgetStylePicker(
|
val current = style ?: return
|
||||||
title = stringResource(R.string.widget_analog_config_title),
|
AnalogClockConfigScreen(
|
||||||
options = AnalogClockStyle.entries,
|
style = current,
|
||||||
selected = style,
|
onStyleChange = { style = it },
|
||||||
label = { stringResource(analogStyleLabel(it)) },
|
tapTarget = tapTarget,
|
||||||
preview = { AnalogStylePreview(it) },
|
onTapTargetChange = { tapTarget = it },
|
||||||
onSelect = { picked ->
|
onSave = {
|
||||||
style = picked
|
|
||||||
save {
|
save {
|
||||||
updateAppWidgetState(this@AnalogClockConfigActivity, glanceId) { prefs ->
|
updateAppWidgetState(this@AnalogClockConfigActivity, glanceId) { prefs ->
|
||||||
prefs[WIDGET_ANALOG_STYLE_KEY] = picked.name
|
prefs[WIDGET_ANALOG_STYLE_KEY] = current.name
|
||||||
|
prefs[WIDGET_TAP_TARGET_KEY] = tapTarget.name
|
||||||
}
|
}
|
||||||
AnalogClockWidget().update(this@AnalogClockConfigActivity, glanceId)
|
AnalogClockWidget().update(this@AnalogClockConfigActivity, glanceId)
|
||||||
}
|
}
|
||||||
@@ -59,6 +79,52 @@ class AnalogClockConfigActivity : WidgetConfigActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AnalogClockConfigScreen(
|
||||||
|
style: AnalogClockStyle,
|
||||||
|
onStyleChange: (AnalogClockStyle) -> Unit,
|
||||||
|
tapTarget: ClockWidgetTapTarget,
|
||||||
|
onTapTargetChange: (ClockWidgetTapTarget) -> Unit,
|
||||||
|
onSave: () -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
var showStylePicker by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
FullScreenPicker(
|
||||||
|
title = stringResource(R.string.widget_analog_config_title),
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
actions = {
|
||||||
|
TextButton(onClick = onSave) { Text(stringResource(android.R.string.ok)) }
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
// 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)),
|
||||||
|
position = Position.Alone,
|
||||||
|
onClick = { showStylePicker = true },
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
TapTargetRow(target = tapTarget, position = Position.Alone, onTargetChange = onTapTargetChange)
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showStylePicker) {
|
||||||
|
WidgetStylePicker(
|
||||||
|
title = stringResource(R.string.widget_config_style),
|
||||||
|
options = AnalogClockStyle.entries,
|
||||||
|
selected = style,
|
||||||
|
label = { stringResource(analogStyleLabel(it)) },
|
||||||
|
preview = { AnalogStylePreview(it) },
|
||||||
|
onSelect = onStyleChange,
|
||||||
|
onDismiss = { showStylePicker = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal fun analogStyleLabel(style: AnalogClockStyle): Int = when (style) {
|
internal fun analogStyleLabel(style: AnalogClockStyle): Int = when (style) {
|
||||||
AnalogClockStyle.ROUND -> R.string.analog_style_round
|
AnalogClockStyle.ROUND -> R.string.analog_style_round
|
||||||
AnalogClockStyle.NUMERAL -> R.string.analog_style_numeral
|
AnalogClockStyle.NUMERAL -> R.string.analog_style_numeral
|
||||||
|
|||||||
+13
-1
@@ -30,8 +30,10 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
import de.jeanlucmakiola.clockula.R
|
import de.jeanlucmakiola.clockula.R
|
||||||
import de.jeanlucmakiola.clockula.data.worldclocks.WorldClockRepository
|
import de.jeanlucmakiola.clockula.data.worldclocks.WorldClockRepository
|
||||||
import de.jeanlucmakiola.clockula.data.zones.ZoneNames
|
import de.jeanlucmakiola.clockula.data.zones.ZoneNames
|
||||||
|
import de.jeanlucmakiola.clockula.domain.widget.ClockWidgetTapTarget
|
||||||
import de.jeanlucmakiola.clockula.domain.widget.DigitalClockSettings
|
import de.jeanlucmakiola.clockula.domain.widget.DigitalClockSettings
|
||||||
import de.jeanlucmakiola.clockula.domain.widget.DigitalClockStyle
|
import de.jeanlucmakiola.clockula.domain.widget.DigitalClockStyle
|
||||||
|
import de.jeanlucmakiola.clockula.domain.widget.parseClockWidgetTapTarget
|
||||||
import de.jeanlucmakiola.clockula.domain.widget.showsWorldClocks
|
import de.jeanlucmakiola.clockula.domain.widget.showsWorldClocks
|
||||||
import de.jeanlucmakiola.clockula.ui.widget.ClockWidgetCity
|
import de.jeanlucmakiola.clockula.ui.widget.ClockWidgetCity
|
||||||
import de.jeanlucmakiola.clockula.ui.widget.digitalClockViews
|
import de.jeanlucmakiola.clockula.ui.widget.digitalClockViews
|
||||||
@@ -40,13 +42,14 @@ import de.jeanlucmakiola.clockula.widget.ClockWidget
|
|||||||
import de.jeanlucmakiola.clockula.widget.WIDGET_DIGITAL_STYLE_KEY
|
import de.jeanlucmakiola.clockula.widget.WIDGET_DIGITAL_STYLE_KEY
|
||||||
import de.jeanlucmakiola.clockula.widget.WIDGET_SHOW_DATE_KEY
|
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_SHOW_WORLD_CLOCKS_KEY
|
||||||
|
import de.jeanlucmakiola.clockula.widget.WIDGET_TAP_TARGET_KEY
|
||||||
import de.jeanlucmakiola.clockula.widget.parseDigitalClockSettings
|
import de.jeanlucmakiola.clockula.widget.parseDigitalClockSettings
|
||||||
import de.jeanlucmakiola.floret.components.FullScreenPicker
|
import de.jeanlucmakiola.floret.components.FullScreenPicker
|
||||||
import de.jeanlucmakiola.floret.components.GroupedRow
|
import de.jeanlucmakiola.floret.components.GroupedRow
|
||||||
import de.jeanlucmakiola.floret.components.Position
|
import de.jeanlucmakiola.floret.components.Position
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/** Picks the digital widget instance's style and the two switches. */
|
/** Picks the digital widget instance's style, the two switches, and the tab a tap opens. */
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ClockWidgetConfigActivity : WidgetConfigActivity() {
|
class ClockWidgetConfigActivity : WidgetConfigActivity() {
|
||||||
|
|
||||||
@@ -59,11 +62,13 @@ class ClockWidgetConfigActivity : WidgetConfigActivity() {
|
|||||||
// (and its OK) only appears then, so neither an early save nor a
|
// (and its OK) only appears then, so neither an early save nor a
|
||||||
// late load can write defaults over what the user has.
|
// late load can write defaults over what the user has.
|
||||||
var settings by remember { mutableStateOf<DigitalClockSettings?>(null) }
|
var settings by remember { mutableStateOf<DigitalClockSettings?>(null) }
|
||||||
|
var tapTarget by remember { mutableStateOf(ClockWidgetTapTarget.ALARMS) }
|
||||||
val cities by produceState(initialValue = emptyList<ClockWidgetCity>()) {
|
val cities by produceState(initialValue = emptyList<ClockWidgetCity>()) {
|
||||||
value = widgetCities(worldClockRepository, zoneNames)
|
value = widgetCities(worldClockRepository, zoneNames)
|
||||||
}
|
}
|
||||||
LaunchedEffect(glanceId) {
|
LaunchedEffect(glanceId) {
|
||||||
val state = getAppWidgetState(this@ClockWidgetConfigActivity, PreferencesGlanceStateDefinition, glanceId)
|
val state = getAppWidgetState(this@ClockWidgetConfigActivity, PreferencesGlanceStateDefinition, glanceId)
|
||||||
|
tapTarget = parseClockWidgetTapTarget(state[WIDGET_TAP_TARGET_KEY])
|
||||||
settings = parseDigitalClockSettings(
|
settings = parseDigitalClockSettings(
|
||||||
style = state[WIDGET_DIGITAL_STYLE_KEY],
|
style = state[WIDGET_DIGITAL_STYLE_KEY],
|
||||||
showDate = state[WIDGET_SHOW_DATE_KEY],
|
showDate = state[WIDGET_SHOW_DATE_KEY],
|
||||||
@@ -75,12 +80,15 @@ class ClockWidgetConfigActivity : WidgetConfigActivity() {
|
|||||||
settings = current,
|
settings = current,
|
||||||
cities = cities,
|
cities = cities,
|
||||||
onSettingsChange = { settings = it },
|
onSettingsChange = { settings = it },
|
||||||
|
tapTarget = tapTarget,
|
||||||
|
onTapTargetChange = { tapTarget = it },
|
||||||
onSave = {
|
onSave = {
|
||||||
save {
|
save {
|
||||||
updateAppWidgetState(this@ClockWidgetConfigActivity, glanceId) { prefs ->
|
updateAppWidgetState(this@ClockWidgetConfigActivity, glanceId) { prefs ->
|
||||||
prefs[WIDGET_DIGITAL_STYLE_KEY] = current.style.name
|
prefs[WIDGET_DIGITAL_STYLE_KEY] = current.style.name
|
||||||
prefs[WIDGET_SHOW_DATE_KEY] = current.showDate
|
prefs[WIDGET_SHOW_DATE_KEY] = current.showDate
|
||||||
prefs[WIDGET_SHOW_WORLD_CLOCKS_KEY] = current.showWorldClocks
|
prefs[WIDGET_SHOW_WORLD_CLOCKS_KEY] = current.showWorldClocks
|
||||||
|
prefs[WIDGET_TAP_TARGET_KEY] = tapTarget.name
|
||||||
}
|
}
|
||||||
ClockWidget().update(this@ClockWidgetConfigActivity, glanceId)
|
ClockWidget().update(this@ClockWidgetConfigActivity, glanceId)
|
||||||
}
|
}
|
||||||
@@ -95,6 +103,8 @@ private fun ClockWidgetConfigScreen(
|
|||||||
settings: DigitalClockSettings,
|
settings: DigitalClockSettings,
|
||||||
cities: List<ClockWidgetCity>,
|
cities: List<ClockWidgetCity>,
|
||||||
onSettingsChange: (DigitalClockSettings) -> Unit,
|
onSettingsChange: (DigitalClockSettings) -> Unit,
|
||||||
|
tapTarget: ClockWidgetTapTarget,
|
||||||
|
onTapTargetChange: (ClockWidgetTapTarget) -> Unit,
|
||||||
onSave: () -> Unit,
|
onSave: () -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -134,6 +144,8 @@ private fun ClockWidgetConfigScreen(
|
|||||||
onClick = { onSettingsChange(settings.copy(showWorldClocks = !settings.showWorldClocks)) },
|
onClick = { onSettingsChange(settings.copy(showWorldClocks = !settings.showWorldClocks)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
TapTargetRow(target = tapTarget, position = Position.Alone, onTargetChange = onTapTargetChange)
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-3
@@ -35,9 +35,8 @@ import de.jeanlucmakiola.clockula.widget.WIDGET_STOPWATCH_STYLE_KEY
|
|||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The stopwatch widget's entire configuration: a style pick. Like
|
* The stopwatch widget's entire configuration: a style pick. Tapping an
|
||||||
* [AnalogClockConfigActivity], tapping an option saves and finishes
|
* option saves and finishes immediately.
|
||||||
* immediately.
|
|
||||||
*/
|
*/
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class StopwatchConfigActivity : WidgetConfigActivity() {
|
class StopwatchConfigActivity : WidgetConfigActivity() {
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package de.jeanlucmakiola.clockula.ui.widget.config
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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 de.jeanlucmakiola.clockula.R
|
||||||
|
import de.jeanlucmakiola.clockula.domain.widget.ClockWidgetTapTarget
|
||||||
|
import de.jeanlucmakiola.clockula.ui.widget.destination
|
||||||
|
import de.jeanlucmakiola.floret.components.GroupedRow
|
||||||
|
import de.jeanlucmakiola.floret.components.OptionPicker
|
||||||
|
import de.jeanlucmakiola.floret.components.Position
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A clock widget's "On tap" setting (#8): a row summarising which tab a tap
|
||||||
|
* opens, and a picker of the four tabs behind it, named exactly as the
|
||||||
|
* navigation bar names them.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
internal fun TapTargetRow(
|
||||||
|
target: ClockWidgetTapTarget,
|
||||||
|
position: Position,
|
||||||
|
onTargetChange: (ClockWidgetTapTarget) -> Unit,
|
||||||
|
) {
|
||||||
|
var showPicker by remember { mutableStateOf(false) }
|
||||||
|
GroupedRow(
|
||||||
|
title = stringResource(R.string.widget_config_on_tap),
|
||||||
|
summary = stringResource(target.destination.titleRes),
|
||||||
|
position = position,
|
||||||
|
onClick = { showPicker = true },
|
||||||
|
)
|
||||||
|
if (showPicker) {
|
||||||
|
OptionPicker(
|
||||||
|
title = stringResource(R.string.widget_config_on_tap),
|
||||||
|
options = ClockWidgetTapTarget.entries,
|
||||||
|
selected = target,
|
||||||
|
label = { stringResource(it.destination.titleRes) },
|
||||||
|
onSelect = onTargetChange,
|
||||||
|
onDismiss = { showPicker = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,12 @@ 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 tab a clock widget — digital or
|
||||||
|
* analog — opens on tap: a `ClockWidgetTapTarget` ([name]).
|
||||||
|
*/
|
||||||
|
internal val WIDGET_TAP_TARGET_KEY = stringPreferencesKey("tap_target")
|
||||||
|
|
||||||
/** Per-instance Glance state key holding the stopwatch widget's `StopwatchWidgetStyle` ([name]). */
|
/** Per-instance Glance state key holding the stopwatch widget's `StopwatchWidgetStyle` ([name]). */
|
||||||
internal val WIDGET_STOPWATCH_STYLE_KEY = stringPreferencesKey("stopwatch_style")
|
internal val WIDGET_STOPWATCH_STYLE_KEY = stringPreferencesKey("stopwatch_style")
|
||||||
|
|
||||||
|
|||||||
@@ -398,6 +398,9 @@
|
|||||||
<string name="widget_config_show_world_clocks">World clocks</string>
|
<string name="widget_config_show_world_clocks">World clocks</string>
|
||||||
<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>
|
||||||
|
|
||||||
|
<!-- #8: the tab a clock widget opens when tapped; its options are the tab names -->
|
||||||
|
<string name="widget_config_on_tap">On tap</string>
|
||||||
|
|
||||||
<!-- M11: analog clock widget -->
|
<!-- M11: analog clock widget -->
|
||||||
<string name="widget_analog_label">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>
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package de.jeanlucmakiola.clockula.domain.widget
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class ClockWidgetTapTargetTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `null parses to Alarms, so clock widgets placed before the setting existed open Alarms`() {
|
||||||
|
assertThat(parseClockWidgetTapTarget(null)).isEqualTo(ClockWidgetTapTarget.ALARMS)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `an unknown name parses to Alarms`() {
|
||||||
|
assertThat(parseClockWidgetTapTarget("Nonexistent")).isEqualTo(ClockWidgetTapTarget.ALARMS)
|
||||||
|
assertThat(parseClockWidgetTapTarget("")).isEqualTo(ClockWidgetTapTarget.ALARMS)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `each tap target's own name round-trips`() {
|
||||||
|
ClockWidgetTapTarget.entries.forEach { target ->
|
||||||
|
assertThat(parseClockWidgetTapTarget(target.name)).isEqualTo(target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -238,4 +238,24 @@ class ShellNavigationTest {
|
|||||||
|
|
||||||
assertThat(answers).containsExactly(ClockulaDestination.TIMERS, null, null).inOrder()
|
assertThat(answers).containsExactly(ClockulaDestination.TIMERS, null, null).inOrder()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- #8: a widget picks the tab it opens ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `every tab's show action asks for that tab`() {
|
||||||
|
val resolved = ClockulaDestination.entries.map {
|
||||||
|
ShellNavigation.tabForAction(ShellNavigation.showActionOf(it))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(resolved).isEqualTo(ClockulaDestination.entries.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `every tab's show action opens it as a plain tab request`() {
|
||||||
|
val requests = ClockulaDestination.entries.map {
|
||||||
|
ShellNavigation.requestFor(ShellNavigation.showActionOf(it), null, emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(requests).isEqualTo(ClockulaDestination.entries.map(ShellRequest::OpenTab))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package de.jeanlucmakiola.clockula.ui.widget
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import de.jeanlucmakiola.clockula.domain.widget.ClockWidgetTapTarget
|
||||||
|
import de.jeanlucmakiola.clockula.domain.widget.parseClockWidgetTapTarget
|
||||||
|
import de.jeanlucmakiola.clockula.ui.shell.ClockulaDestination
|
||||||
|
import de.jeanlucmakiola.clockula.ui.shell.ShellNavigation
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A clock widget's tap (#8) reaches `MainActivity` as an action, and the shell
|
||||||
|
* maps that action back to a tab: the round trip has to come out on the tab
|
||||||
|
* the user picked, for every one of them.
|
||||||
|
*/
|
||||||
|
class ClockTapTargetTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `every tap target is the tab of the same name, in the same order`() {
|
||||||
|
val destinations = ClockWidgetTapTarget.entries.map { it.destination }
|
||||||
|
|
||||||
|
assertThat(destinations).isEqualTo(ClockulaDestination.entries.toList())
|
||||||
|
ClockWidgetTapTarget.entries.forEach { assertThat(it.destination.name).isEqualTo(it.name) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `every tap target's action opens its own tab`() {
|
||||||
|
ClockWidgetTapTarget.entries.forEach { target ->
|
||||||
|
val action = ShellNavigation.showActionOf(target.destination)
|
||||||
|
|
||||||
|
assertThat(ShellNavigation.tabForAction(action)).isEqualTo(target.destination)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a widget with no stored tap target opens alarms`() {
|
||||||
|
assertThat(parseClockWidgetTapTarget(null).destination).isEqualTo(ClockulaDestination.ALARMS)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1334,7 +1334,13 @@ The policy the shell follows is a pure function, `ShellNavigation`:
|
|||||||
one loses nothing;
|
one loses nothing;
|
||||||
- a tab an intent asks for (a notification, a widget, the exported door) is the
|
- a tab an intent asks for (a notification, a widget, the exported door) is the
|
||||||
same pop and a jump — it always lands on the tab itself, never on whatever
|
same pop and a jump — it always lands on the tab itself, never on whatever
|
||||||
nested screen was last open there;
|
nested screen was last open there. Widgets build that intent in one place,
|
||||||
|
`openAppIntent`, with `CLEAR_TOP` + `SINGLE_TOP` beside `NEW_TASK`, so an
|
||||||
|
activity left on top of `MainActivity` in its task (a picker the editor
|
||||||
|
opened) is finished and the intent reaches `onNewIntent` rather than the
|
||||||
|
task merely being brought forward. The two clock widgets open the tab picked
|
||||||
|
per instance in their config screen (`ClockWidgetTapTarget`, stored next to
|
||||||
|
the style; Alarms when unset — #8);
|
||||||
- back from any of the other three tabs returns to **Alarms**; back from Alarms
|
- back from any of the other three tabs returns to **Alarms**; back from Alarms
|
||||||
and from a null route leaves the app, and a nested screen's back belongs to
|
and from a null route leaves the app, and a nested screen's back belongs to
|
||||||
the `NavController`, not to us.
|
the `NavController`, not to us.
|
||||||
|
|||||||
Reference in New Issue
Block a user