Show an agenda in the clock widget's dynamic zone, with multiple slots
The dynamic zone showed exactly one part: the highest ranked of the enabled ones. It now shows up to `clockWidgetDynamicZoneSlots` of them (default 1), adjustable with a slider in the clock widget settings whose maximum is the number of enabled parts. `ClockWidgetVM.getActiveParts()` returns that sorted list, and both clock layouts render it (stacked under the clock, or next to it in the horizontal layout). On top of that the zone has an "Events" part (`clockWidgetCalendarPart`, off by default) that shows today's agenda: events that are still running or upcoming today, plus today's all-day events. At most four rows; when there are more, the last row is a "+N more events" hint that opens the calendar app. Tapping a row opens the event through `CalendarEvent.launch()`. A row is three fixed-width columns that are pure layout, not drawn: a dot in the calendar's colour, the time, and the title (ellipsized when it does not fit). Times are always times, never dates - an all-day event renders "all-day" - and the block is centred under the clock. The columns line up across rows, and the time is left-aligned within its column. The part ranks 70 when an event is running or starts within half an hour, 30 while events are left today and 0 on an empty day, so the date keeps the slot when there is nothing to show. It reloads itself through a ContentObserver on CalendarContract.Events and a 15 minute ticker, because unlike the calendar widget it is on screen all the time. Tasks (`CalendarEvent.isTask`) are filtered out for now; a tasks part can reuse this once tasks are configured. Verified on the Titan 2: with the part enabled and three slots, the home screen shows the agenda (two all-day events and a timed one) above the date part.
This commit is contained in:
40
CLAUDE.md
40
CLAUDE.md
@@ -1,4 +1,4 @@
|
||||
# Kvaesitso (fork with type-to-search)
|
||||
# Kvaesitso (fork with type-to-search and a home-screen agenda)
|
||||
|
||||
Fork of [Kvaesitso](https://github.com/MM2-0/Kvaesitso) that adds type-to-search for phones with a
|
||||
physical keyboard. Built for, and installed on, a Unihertz Titan 2 Elite.
|
||||
@@ -45,6 +45,44 @@ text.
|
||||
The search is also cleared whenever the launcher is left, even for a short app switch — upstream
|
||||
keeps it for returns within five seconds, which leaves a stale query on screen.
|
||||
|
||||
## The clock widget's dynamic zone (agenda part and multiple slots)
|
||||
|
||||
Upstream's home-screen clock has one "dynamic zone" below (or beside) the clock. Every enabled
|
||||
*part* (`DatePartProvider`, `MusicPartProvider`, `BatteryPartProvider`, `AlarmPartProvider`) reports
|
||||
a `getRanking()`; only the single highest-ranked part with a ranking above zero is shown. Two
|
||||
changes on top of that:
|
||||
|
||||
* **The zone can show more than one part.** The setting `clockWidgetDynamicZoneSlots` (1..number of
|
||||
enabled parts) caps how many of the highest-ranked parts are rendered;
|
||||
`ClockWidgetVM.getActiveParts()` returns that sorted list instead of a single provider. The
|
||||
slider lives in `ConfigureClockWidgetSheet` next to the part toggles.
|
||||
* **A new part shows today's agenda.** `CalendarPartProvider`
|
||||
(`app/ui/.../widgets/clock/parts/`) is toggled with the *Events* switch
|
||||
(`clockWidgetCalendarPart`). It lists the events that are still running or upcoming today, plus
|
||||
today's all-day events, capped at 4 rows — if there are more, the last row is a "+N more events"
|
||||
hint that opens the calendar app. Tapping a row opens the event
|
||||
(`CalendarEvent.launch`, i.e. `ACTION_VIEW` on `content://com.android.calendar/events/<id>`,
|
||||
which KashCal handles). Rows reuse `CalendarEvent.getSummary()` from
|
||||
`ui/launcher/search/calendar/CalendarItem.kt` (made `internal` for this), so an all-day event
|
||||
reads "All day" and a timed one reads e.g. "09:00 – 09:30".
|
||||
|
||||
Rankings, in the order the zone prefers them: `CalendarPartProvider` 70 when an event is running or
|
||||
starts within 30 minutes, else 30 while there are events left today, else 0 (so the date part keeps
|
||||
the slot on an empty day); alarms 60 (ringing within 8 h); media 80 while playing; battery 55 when
|
||||
low and 10 when charging or set to "always"; date 1. Tasks (`CalendarEvent.isTask`) are filtered out
|
||||
of the agenda; a tasks part is planned but not implemented.
|
||||
|
||||
`CalendarPartProvider` keeps itself up to date with a `ContentObserver` on
|
||||
`CalendarContract.Events.CONTENT_URI` plus a 15-minute ticker (the other calendar consumers in the
|
||||
launcher only re-query when they become visible). `PartProvider.setTime()` is called every second
|
||||
by the clock widget; the provider folds that into its ranking instead of re-querying.
|
||||
|
||||
The clock settings themselves are in `core/preferences`: `LauncherSettingsData` (new fields
|
||||
`clockWidgetCalendarPart`, `clockWidgetDynamicZoneSlots`), `ClockWidgetParts` and the
|
||||
`ClockWidgetSettings` accessors in `core/preferences/.../ui/ClockWidgetSettings.kt`, and the UI
|
||||
state in `ui/settings/clockwidget/ClockWidgetSettingsScreenVM.kt`. New strings are English-only in
|
||||
`core/i18n/src/main/res/values/strings.xml`; translations come from Crowdin upstream.
|
||||
|
||||
## Building
|
||||
|
||||
On the desktop:
|
||||
|
||||
@@ -63,6 +63,7 @@ import de.mm20.launcher2.ui.component.DismissableBottomSheet
|
||||
import de.mm20.launcher2.ui.component.preferences.Preference
|
||||
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
|
||||
import de.mm20.launcher2.ui.component.preferences.ListPreference
|
||||
import de.mm20.launcher2.ui.component.preferences.SliderPreference
|
||||
import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.AnalogClock
|
||||
import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.BinaryClock
|
||||
import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.CustomClock
|
||||
@@ -105,8 +106,8 @@ fun ClockWidget(
|
||||
viewModel.updateTime(time)
|
||||
}
|
||||
|
||||
val partProvider by remember { viewModel.getActivePart(context) }.collectAsStateWithLifecycle(
|
||||
null
|
||||
val partProviders by remember { viewModel.getActiveParts(context) }.collectAsStateWithLifecycle(
|
||||
emptyList()
|
||||
)
|
||||
|
||||
AnimatedContent(editMode, label = "ClockWidget") {
|
||||
@@ -181,11 +182,11 @@ fun ClockWidget(
|
||||
Clock(clockStyle, false, darkColors)
|
||||
}
|
||||
|
||||
if (partProvider != null) {
|
||||
partProviders.forEach { provider ->
|
||||
DynamicZone(
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
compact = false,
|
||||
provider = partProvider,
|
||||
provider = provider,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -198,12 +199,13 @@ fun ClockWidget(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
if (partProvider != null) {
|
||||
DynamicZone(
|
||||
modifier = Modifier.weight(1f),
|
||||
compact = true,
|
||||
provider = partProvider,
|
||||
)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
partProviders.forEach { provider ->
|
||||
DynamicZone(
|
||||
compact = true,
|
||||
provider = provider,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (clockStyle !is ClockWidgetStyle.Empty) {
|
||||
Box(
|
||||
@@ -362,6 +364,8 @@ fun ConfigureClockWidgetSheet(
|
||||
val useAccentColor by viewModel.useThemeColor.collectAsState()
|
||||
val parts by viewModel.parts.collectAsState()
|
||||
val smartspacer by viewModel.useSmartspacer.collectAsState()
|
||||
val slots by viewModel.slots.collectAsState()
|
||||
val partCount by viewModel.partCount.collectAsState()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -668,6 +672,15 @@ fun ConfigureClockWidgetSheet(
|
||||
viewModel.setAlarmPart(it)
|
||||
}
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(R.string.preference_clockwidget_calendar_part),
|
||||
summary = stringResource(R.string.preference_clockwidget_calendar_part_summary),
|
||||
icon = R.drawable.event_24px,
|
||||
value = parts?.calendar == true,
|
||||
onValueChanged = {
|
||||
viewModel.setCalendarPart(it)
|
||||
}
|
||||
)
|
||||
ListPreference(
|
||||
title = stringResource(R.string.preference_clockwidget_battery_part),
|
||||
icon = R.drawable.battery_full_24px,
|
||||
@@ -681,6 +694,18 @@ fun ConfigureClockWidgetSheet(
|
||||
stringResource(R.string.preference_clockwidget_battery_part_always_show) to BatteryStatusVisibility.Always
|
||||
)
|
||||
)
|
||||
SliderPreference(
|
||||
title = stringResource(R.string.preference_clockwidget_dynamic_zone_slots),
|
||||
icon = R.drawable.dashboard_2_24px,
|
||||
value = slots,
|
||||
min = 1,
|
||||
max = maxOf(1, partCount),
|
||||
step = 1,
|
||||
enabled = partCount > 1,
|
||||
onValueChanged = {
|
||||
viewModel.setSlots(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.preferences.ui.ClockWidgetSettings
|
||||
import de.mm20.launcher2.ui.launcher.widgets.clock.parts.AlarmPartProvider
|
||||
import de.mm20.launcher2.ui.launcher.widgets.clock.parts.BatteryPartProvider
|
||||
import de.mm20.launcher2.ui.launcher.widgets.clock.parts.CalendarPartProvider
|
||||
import de.mm20.launcher2.ui.launcher.widgets.clock.parts.DatePartProvider
|
||||
import de.mm20.launcher2.ui.launcher.widgets.clock.parts.FavoritesPartProvider
|
||||
import de.mm20.launcher2.ui.launcher.widgets.clock.parts.MusicPartProvider
|
||||
@@ -37,21 +38,27 @@ class ClockWidgetVM : ViewModel(), KoinComponent {
|
||||
|
||||
val providers = mutableListOf<PartProvider>()
|
||||
if (parts.date) providers += DatePartProvider()
|
||||
if (parts.calendar) providers += CalendarPartProvider()
|
||||
if (parts.music) providers += MusicPartProvider()
|
||||
providers += BatteryPartProvider(parts.battery)
|
||||
if (parts.alarm) providers += AlarmPartProvider()
|
||||
providers
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
|
||||
|
||||
fun getActivePart(context: Context): Flow<PartProvider?> = channelFlow {
|
||||
fun getActiveParts(context: Context): Flow<List<PartProvider>> = channelFlow {
|
||||
partProviders.collectLatest { providers ->
|
||||
if (providers.isEmpty()) {
|
||||
send(null)
|
||||
send(emptyList())
|
||||
return@collectLatest
|
||||
}
|
||||
val rankings = providers.map { it.getRanking(context).map { r -> r to it } }
|
||||
combine(rankings) { r ->
|
||||
r.filter { it.first > 0 }.maxByOrNull { it.first }?.second
|
||||
val ranked = combine(rankings) { r ->
|
||||
r.filter { it.first > 0 }
|
||||
.sortedByDescending { it.first }
|
||||
.map { it.second }
|
||||
}
|
||||
combine(ranked, settings.slots) { r, slots ->
|
||||
r.take(slots.coerceAtLeast(1))
|
||||
}.collectLatest {
|
||||
send(it)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
package de.mm20.launcher2.ui.launcher.widgets.clock.parts
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.database.ContentObserver
|
||||
import android.icu.text.DateFormat
|
||||
import android.icu.util.ULocale
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.CalendarContract
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.calendar.CalendarRepository
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.preferences.TimeFormat
|
||||
import de.mm20.launcher2.preferences.search.CalendarSearchSettings
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.locals.LocalTimeFormat
|
||||
import de.mm20.launcher2.ui.utils.isTwentyFourHours
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.time.LocalDate
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Highest number of rows the agenda shows. If there are more events than this, the last
|
||||
* row is a "+N more events" hint.
|
||||
*/
|
||||
private const val MaxRows = 4
|
||||
|
||||
/** An event that starts within this time frame (or is running) raises the ranking. */
|
||||
private const val SoonThreshold = 30 * 60 * 1000L
|
||||
|
||||
/** Re-query interval, so the agenda survives calendar changes that don't notify. */
|
||||
private const val RefreshInterval = 15 * 60 * 1000L
|
||||
|
||||
private const val RankingToday = 30
|
||||
private const val RankingSoon = 70
|
||||
|
||||
/** Width of the dot column. Kept fixed so the rows line up with each other. */
|
||||
private val DotColumn = 12.dp
|
||||
|
||||
/** Width of the time column. Wide enough for "11:59 PM" and "all-day". */
|
||||
private val TimeColumn = 64.dp
|
||||
|
||||
/** Width of the title column. Titles longer than this are ellipsized. */
|
||||
private val TitleColumn = 176.dp
|
||||
|
||||
/**
|
||||
* Dynamic zone part that shows today's agenda.
|
||||
*
|
||||
* Only events that are still running or upcoming today are shown, plus all-day events of
|
||||
* today. Tasks are ignored for now.
|
||||
*/
|
||||
class CalendarPartProvider : PartProvider, KoinComponent {
|
||||
|
||||
private val calendarRepository: CalendarRepository by inject()
|
||||
private val permissionsManager: PermissionsManager by inject()
|
||||
private val searchSettings: CalendarSearchSettings by inject()
|
||||
|
||||
private val agenda = MutableStateFlow<List<CalendarEvent>>(emptyList())
|
||||
private val time = MutableStateFlow(System.currentTimeMillis())
|
||||
|
||||
override fun setTime(time: Long) {
|
||||
this.time.value = time
|
||||
}
|
||||
|
||||
override fun getRanking(context: Context): Flow<Int> = channelFlow {
|
||||
// Load the agenda in the background; the ranking itself only depends on the
|
||||
// already known agenda and the current time, so it can be emitted right away.
|
||||
launch { observeAgenda(context) }
|
||||
combine(agenda, time) { events, now -> ranking(events, now) }
|
||||
.distinctUntilChanged()
|
||||
.collect { send(it) }
|
||||
}
|
||||
|
||||
private suspend fun observeAgenda(context: Context) {
|
||||
combine(
|
||||
dayChanges(context),
|
||||
permissionsManager.hasPermission(PermissionGroup.Calendar),
|
||||
searchSettings.excludedCalendars,
|
||||
) { now, hasPermission, excludedCalendars -> Triple(now, hasPermission, excludedCalendars) }
|
||||
.collectLatest { (now, hasPermission, excludedCalendars) ->
|
||||
agenda.value = if (!hasPermission) {
|
||||
emptyList()
|
||||
} else withContext(Dispatchers.IO) {
|
||||
calendarRepository.findMany(
|
||||
from = now,
|
||||
to = endOfDay(now),
|
||||
excludeCalendars = excludedCalendars.toList(),
|
||||
).first()
|
||||
.filter { !it.isTask }
|
||||
.sortedBy { it.startTime ?: it.endTime }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits the current time when the calendar database changes and periodically, so the
|
||||
* agenda is re-queried when events change or the day rolls over.
|
||||
*/
|
||||
private fun dayChanges(context: Context): Flow<Long> = callbackFlow {
|
||||
val observer = object : ContentObserver(Handler(Looper.getMainLooper())) {
|
||||
override fun onChange(selfChange: Boolean) {
|
||||
trySend(System.currentTimeMillis())
|
||||
}
|
||||
}
|
||||
val registered = try {
|
||||
context.contentResolver.registerContentObserver(
|
||||
CalendarContract.Events.CONTENT_URI, true, observer
|
||||
)
|
||||
true
|
||||
} catch (e: SecurityException) {
|
||||
false
|
||||
}
|
||||
trySend(System.currentTimeMillis())
|
||||
val ticker = launch {
|
||||
while (isActive) {
|
||||
delay(RefreshInterval)
|
||||
trySend(System.currentTimeMillis())
|
||||
}
|
||||
}
|
||||
awaitClose {
|
||||
ticker.cancel()
|
||||
if (registered) context.contentResolver.unregisterContentObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ranking(events: List<CalendarEvent>, now: Long): Int {
|
||||
if (events.isEmpty()) return 0
|
||||
val soon = events.any { event ->
|
||||
val start = event.startTime
|
||||
!event.allDay && start != null && start <= now + SoonThreshold && event.endTime > now
|
||||
}
|
||||
return if (soon) RankingSoon else RankingToday
|
||||
}
|
||||
|
||||
private fun endOfDay(now: Long): Long {
|
||||
val offset = OffsetDateTime.now().offset
|
||||
return LocalDate.now().plusDays(1).atStartOfDay().toEpochSecond(offset) * 1000
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Component(compactLayout: Boolean) {
|
||||
val events by agenda.collectAsState()
|
||||
val context = LocalContext.current
|
||||
if (events.isEmpty()) return
|
||||
|
||||
val timeFormat = LocalTimeFormat.current
|
||||
val contentColor = LocalContentColor.current
|
||||
|
||||
val visibleCount = if (events.size > MaxRows) MaxRows - 1 else events.size
|
||||
val hiddenCount = events.size - visibleCount
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
for (event in events.take(visibleCount)) {
|
||||
AgendaRow(
|
||||
color = event.color?.let { Color(it) } ?: contentColor,
|
||||
time = event.timeLabel(context, timeFormat),
|
||||
title = event.label,
|
||||
onClick = { event.launch(context, null) },
|
||||
)
|
||||
}
|
||||
if (hiddenCount > 0) {
|
||||
AgendaRow(
|
||||
color = null,
|
||||
time = null,
|
||||
title = pluralStringResource(
|
||||
R.plurals.clockwidget_calendar_part_more_events,
|
||||
hiddenCount,
|
||||
hiddenCount
|
||||
),
|
||||
secondary = true,
|
||||
onClick = { openCalendar(context) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The time of an event, or "all-day" for all-day events. Never a date.
|
||||
*/
|
||||
private fun CalendarEvent.timeLabel(context: Context, timeFormat: TimeFormat): String {
|
||||
if (allDay) return context.getString(R.string.clockwidget_calendar_part_all_day)
|
||||
val start = startTime ?: return ""
|
||||
val skeleton = if (timeFormat.isTwentyFourHours(context)) "HH:mm" else "hh:mm a"
|
||||
return DateFormat.getInstanceForSkeleton(skeleton, ULocale.getDefault()).format(Date(start))
|
||||
}
|
||||
|
||||
/**
|
||||
* One agenda row: a dot in the calendar color, the time and the title. The three columns are
|
||||
* plain layout, they are not drawn.
|
||||
*/
|
||||
@Composable
|
||||
private fun AgendaRow(
|
||||
color: Color?,
|
||||
time: String?,
|
||||
title: String,
|
||||
secondary: Boolean = false,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val contentColor = LocalContentColor.current
|
||||
val secondaryColor = contentColor.copy(alpha = contentColor.alpha * 0.7f)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onClick)
|
||||
.padding(vertical = 2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.width(DotColumn),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (color != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.background(color, CircleShape)
|
||||
)
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.width(TimeColumn),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
if (time != null) {
|
||||
Text(
|
||||
text = time,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = secondaryColor,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.width(TitleColumn),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = if (secondary) secondaryColor else contentColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openCalendar(context: Context) {
|
||||
val builder = CalendarContract.CONTENT_URI.buildUpon()
|
||||
builder.appendPath("time")
|
||||
ContentUris.appendId(builder, System.currentTimeMillis())
|
||||
context.tryStartActivity(
|
||||
Intent(Intent.ACTION_VIEW)
|
||||
.setData(builder.build())
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import de.mm20.launcher2.preferences.ui.ClockWidgetSettings
|
||||
import de.mm20.launcher2.preferences.ui.UiSettings
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
@@ -101,6 +102,32 @@ class ClockWidgetSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
settings.setAlarmPart(alarmPart)
|
||||
}
|
||||
|
||||
fun setCalendarPart(calendarPart: Boolean) {
|
||||
settings.setCalendarPart(calendarPart)
|
||||
}
|
||||
|
||||
val slots = settings.slots
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
|
||||
|
||||
fun setSlots(slots: Int) {
|
||||
settings.setSlots(slots)
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of enabled parts. This is the upper bound for the dynamic zone's slot count.
|
||||
*/
|
||||
val partCount = settings.parts
|
||||
.map { parts ->
|
||||
var count = 0
|
||||
if (parts.date) count++
|
||||
if (parts.music) count++
|
||||
if (parts.alarm) count++
|
||||
if (parts.calendar) count++
|
||||
if (parts.battery != BatteryStatusVisibility.Hide) count++
|
||||
count
|
||||
}
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
|
||||
|
||||
val alignment = settings.alignment
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||
|
||||
|
||||
@@ -598,6 +598,7 @@
|
||||
<string name="preference_clock_widget_alignment_center">Center</string>
|
||||
<string name="preference_clock_widget_alignment_bottom">Bottom</string>
|
||||
<string name="preference_clockwidget_dynamic_zone">Dynamic zone</string>
|
||||
<string name="preference_clockwidget_dynamic_zone_slots">Visible parts</string>
|
||||
<string name="preference_clockwidget_smartspacer">Managed by Smartspacer</string>
|
||||
<string name="preference_clockwidget_date_part">Date</string>
|
||||
<string name="preference_clockwidget_date_part_summary">Show the current date</string>
|
||||
@@ -612,6 +613,13 @@
|
||||
<string name="preference_clockwidget_battery_part_hide">Off</string>
|
||||
<string name="preference_clockwidget_alarm_part">Alarms</string>
|
||||
<string name="preference_clockwidget_alarm_part_summary">Show alarms that will ring within the next 8 hours</string>
|
||||
<string name="preference_clockwidget_calendar_part">Events</string>
|
||||
<string name="preference_clockwidget_calendar_part_summary">Show today\'s agenda</string>
|
||||
<string name="clockwidget_calendar_part_all_day">all-day</string>
|
||||
<plurals name="clockwidget_calendar_part_more_events">
|
||||
<item quantity="one">+%d more event</item>
|
||||
<item quantity="other">+%d more events</item>
|
||||
</plurals>
|
||||
<string name="preference_screen_backup">Backup and restore</string>
|
||||
<string name="preference_screen_backup_summary">Export and import launcher data</string>
|
||||
<string name="preference_backup">Backup</string>
|
||||
|
||||
@@ -58,6 +58,8 @@ data class LauncherSettingsData internal constructor(
|
||||
val clockWidgetBatteryPart: BatteryStatusVisibility = BatteryStatusVisibility.Show,
|
||||
val clockWidgetMusicPart: Boolean = true,
|
||||
val clockWidgetDatePart: Boolean = true,
|
||||
val clockWidgetCalendarPart: Boolean = false,
|
||||
val clockWidgetDynamicZoneSlots: Int = 1,
|
||||
val clockWidgetFillHeight: Boolean = false,
|
||||
val clockWidgetAlignment: ClockWidgetAlignment = ClockWidgetAlignment.Bottom,
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ data class ClockWidgetParts(
|
||||
val music: Boolean = false,
|
||||
val battery: BatteryStatusVisibility = BatteryStatusVisibility.Hide,
|
||||
val alarm: Boolean = false,
|
||||
val calendar: Boolean = false,
|
||||
)
|
||||
|
||||
class ClockWidgetSettings internal constructor(
|
||||
@@ -37,9 +38,29 @@ class ClockWidgetSettings internal constructor(
|
||||
music = it.clockWidgetMusicPart,
|
||||
battery = it.clockWidgetBatteryPart,
|
||||
alarm = it.clockWidgetAlarmPart,
|
||||
calendar = it.clockWidgetCalendarPart,
|
||||
)
|
||||
}.distinctUntilChanged()
|
||||
|
||||
/**
|
||||
* Maximum number of parts the dynamic zone shows at once.
|
||||
*/
|
||||
val slots
|
||||
get() = launcherDataStore.data.map { it.clockWidgetDynamicZoneSlots.coerceAtLeast(1) }
|
||||
.distinctUntilChanged()
|
||||
|
||||
fun setSlots(slots: Int) {
|
||||
launcherDataStore.update {
|
||||
it.copy(clockWidgetDynamicZoneSlots = slots.coerceAtLeast(1))
|
||||
}
|
||||
}
|
||||
|
||||
fun setCalendarPart(calendarPart: Boolean) {
|
||||
launcherDataStore.update {
|
||||
it.copy(clockWidgetCalendarPart = calendarPart)
|
||||
}
|
||||
}
|
||||
|
||||
fun setDatePart(datePart: Boolean) {
|
||||
launcherDataStore.update {
|
||||
it.copy(clockWidgetDatePart = datePart)
|
||||
|
||||
Reference in New Issue
Block a user