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

View File

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

View File

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

View File

@ -6,3 +6,11 @@ data class Quadruple<out A, out B, out C, out D>(
val third: C, val third: C,
val fourth: D 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? val isCompleted: Boolean?
get() = null get() = null
val isTask: Boolean
get() = isCompleted != null
override val preferDetailsOverLaunch: Boolean override val preferDetailsOverLaunch: Boolean
get() = true get() = true

View File

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

View File

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