Calendar widget: separate running tasks from running events

This commit is contained in:
MM20 2025-04-29 23:44:09 +02:00
parent 9e17686c5f
commit f97fb0a88d
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
7 changed files with 79 additions and 19 deletions

View File

@ -366,7 +366,7 @@ fun CalendarItemGridPopup(
private fun CalendarEvent.formatTime(context: Context): String {
val startTime = startTime
if (startTime == null) {
if (startTime == null || isTask) {
if (allDay) {
return DateUtils.formatDateRange(
context,
@ -400,7 +400,7 @@ private fun CalendarEvent.formatTime(context: Context): String {
private fun CalendarEvent.getSummary(context: Context): String {
val startTime = startTime
if (startTime == null) {
if (isTask || startTime == null) {
val isToday = DateUtils.isToday(endTime)
if (isToday) {
if (allDay) {

View File

@ -52,6 +52,7 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.Quadruple
import de.mm20.launcher2.Quintuple
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.InnerCard
import de.mm20.launcher2.ui.component.MissingPermissionBanner
@ -132,6 +133,7 @@ fun CalendarWidget(
val events by viewModel.calendarEvents
val nextEvents by viewModel.nextEvents
val runningEvents by viewModel.hiddenPastEvents
val runningTasks by viewModel.hiddenRunningTasks
val hasPermission by viewModel.hasPermission.collectAsState()
Column {
if (hasPermission == false) {
@ -145,10 +147,11 @@ fun CalendarWidget(
)
}
AnimatedContent(
Quadruple(
Quintuple(
selectedDate,
events,
runningEvents,
runningTasks,
nextEvents
),
transitionSpec = {
@ -185,7 +188,7 @@ fun CalendarWidget(
}
}
}
) { (_, events, runningEvents, nextEvents) ->
) { (_, events, runningEvents, runningTasks, nextEvents) ->
Column(
modifier = Modifier
.padding(horizontal = 12.dp)
@ -212,6 +215,20 @@ fun CalendarWidget(
modifier = Modifier.padding(top = 8.dp)
)
}
if (runningTasks > 0) {
Info(
text = pluralStringResource(
R.plurals.calendar_widget_running_tasks,
runningTasks,
runningTasks
),
onClick = {
viewModel.showAllTasks()
},
modifier = Modifier.padding(top = 8.dp)
)
}
if (nextEvents.isNotEmpty()) {
Text(
stringResource(R.string.calendar_widget_next_events),
@ -272,15 +289,10 @@ private fun Info(
.fillMaxWidth()
.clip(MaterialTheme.shapes.small)
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, MaterialTheme.shapes.small)
.clickable(enabled = onClick != null, onClick = { onClick?.invoke() })
.padding(12.dp)
) {
Box(
contentAlignment = Alignment.CenterStart,
modifier = Modifier
.clickable(enabled = onClick != null, onClick = { onClick?.invoke() })
.padding(12.dp)
) {
Text(text, style = MaterialTheme.typography.bodySmall)
}
Text(text, style = MaterialTheme.typography.bodySmall)
}
}

View File

@ -54,7 +54,9 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
private var showRunningPastDayEvents = false
private var showRunningTasks = false
val hiddenPastEvents = mutableStateOf(0)
val hiddenRunningTasks = mutableStateOf(0)
val selectedDate = mutableStateOf(LocalDate.now())
@ -102,6 +104,7 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
fun selectDate(date: LocalDate) {
val dates = availableDates
showRunningPastDayEvents = false
showRunningTasks = false
if (dates.contains(date)) {
selectedDate.value = date
updateEvents()
@ -113,6 +116,11 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
updateEvents()
}
fun showAllTasks() {
showRunningTasks = true
updateEvents()
}
fun createEvent(context: Context) {
val intent = Intent(Intent.ACTION_EDIT)
intent.data = CalendarContract.Events.CONTENT_URI
@ -136,15 +144,23 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
val dayStart = max(now, date.atStartOfDay().toEpochSecond(offset) * 1000)
val dayEnd = date.plusDays(1).atStartOfDay().toEpochSecond(offset) * 1000
var events = upcomingEvents.filter {
it.endTime >= dayStart && (it.startTime ?: it.endTime) < dayEnd
if (it.isTask && it.isCompleted == true) {
it.endTime >= dayStart && it.endTime < dayEnd
} else {
it.endTime >= dayStart && (it.startTime ?: 0L) < dayEnd
}
}
val startOfDay = date.atStartOfDay().toEpochSecond(offset) * 1000
val startOfNextDay = date.atStartOfDay().plusDays(1).toEpochSecond(offset) * 1000
if (!showRunningPastDayEvents) {
val totalCount = events.size
events = events.filter {
(it.startTime != null && it.startTime!! >= date.atStartOfDay().toEpochSecond(offset) * 1000) ||
it.endTime < date.atStartOfDay().plusDays(1).toEpochSecond(offset) * 1000
((it.startTime != null && it.startTime!! >= startOfDay) ||
it.endTime < startOfNextDay) || it.isTask
}
val hiddenCount = totalCount - events.size
@ -152,11 +168,27 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
} else {
hiddenPastEvents.value = 0
}
if (!showRunningTasks) {
val totalCount = events.size
events = events.filter {
((it.startTime != null && it.startTime!! >= startOfDay) ||
it.endTime < startOfNextDay) || !it.isTask
}
val hiddenCount = totalCount - events.size
hiddenRunningTasks.value = hiddenCount
} else {
hiddenRunningTasks.value = 0
}
calendarEvents.value = events
val e = this.upcomingEvents
if (events.isEmpty() && e.isNotEmpty()) {
nextEvents.value = listOf(e[0])
nextEvents.value = listOfNotNull(
e.sortedBy { if (it.isTask) it.endTime else (it.startTime ?: 0L) }
.find { now < if (it.isTask) it.endTime else (it.startTime ?: 0L) }
)
} else {
nextEvents.value = emptyList()
}

View File

@ -5,4 +5,12 @@ data class Quadruple<out A, out B, out C, out D>(
val second: B,
val third: C,
val fourth: D
)
data class Quintuple<out A, out B, out C, out D, out E>(
val first: A,
val second: B,
val third: C,
val fourth: D,
val fifth: E,
)

View File

@ -22,6 +22,9 @@ interface CalendarEvent : SavableSearchable {
val isCompleted: Boolean?
get() = null
val isTask: Boolean
get() = isCompleted != null
override val preferDetailsOverLaunch: Boolean
get() = true

View File

@ -318,6 +318,10 @@
<item quantity="one">+%1$d running event from past days</item>
<item quantity="other">+%1$d running events from past days</item>
</plurals>
<plurals name="calendar_widget_running_tasks">
<item quantity="one">+%1$d unfinished task</item>
<item quantity="other">+%1$d unfinished tasks</item>
</plurals>
<!-- Calendar events that last the entire day -->
<string name="calendar_event_allday">all-day</string>
<string name="task_due_time">Due at %1$s</string>

View File

@ -24,8 +24,8 @@ internal class TasksCalendarProvider(
return withContext(Dispatchers.IO) {
queryTasks(
selection = buildList {
add("dueDate >= $from")
add("dueDate <= $to")
add("($to >= hideUntil OR hideUntil IS NULL)")
add("$from <= dueDate")
if (excludedCalendars.isNotEmpty()) {
add("cdl_id NOT IN (${excludedCalendars.joinToString()})")
}
@ -82,6 +82,7 @@ internal class TasksCalendarProvider(
cursor?.use {
val idIndex = cursor.getColumnIndex("_id")
val titleIndex = cursor.getColumnIndex("title")
val startIndex = cursor.getColumnIndex("hideUntil")
val dueIndex = cursor.getColumnIndex("dueDate")
val completedIndex = cursor.getColumnIndex("completed")
val notesIndex = cursor.getColumnIndex("notes")
@ -99,7 +100,7 @@ internal class TasksCalendarProvider(
description = cursor.getStringOrNull(notesIndex),
color = cursor.getIntOrNull(colorIndex),
calendarName = cursor.getStringOrNull(calendarNameIndex),
startTime = null,
startTime = cursor.getLongOrNull(startIndex)?.takeIf { it > 0L },
endTime = dueDate,
allDay = dueDate % 60000 <= 0, // https://github.com/tasks/tasks/blob/13d4c029e855fd32ec91e4d4ec5f740ec506136e/data/src/commonMain/kotlin/org/tasks/data/entity/Task.kt#L345
isCompleted = (cursor.getLongOrNull(completedIndex) ?: 0L) != 0L,