Migrate calendar widget to Compose

This commit is contained in:
MM20 2022-02-17 22:52:04 +01:00
parent 386083ede9
commit ca813f3f70
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
7 changed files with 247 additions and 166 deletions

View File

@ -21,9 +21,16 @@ fun ColumnScope.CalendarResults() {
AnimatedVisibility(calendarEvents.isNotEmpty()) {
LauncherCard(
modifier = Modifier.padding(bottom = 8.dp).fillMaxWidth()
modifier = Modifier
.padding(bottom = 8.dp)
.fillMaxWidth()
) {
SearchResultList(items = calendarEvents)
SearchResultList(
items = calendarEvents,
modifier = Modifier
.fillMaxWidth()
.padding(12.dp)
)
}
}
}

View File

@ -1,6 +1,5 @@
package de.mm20.launcher2.ui.launcher.search.common.list
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -11,11 +10,12 @@ import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.data.Searchable
@Composable
fun SearchResultList(items: List<Searchable>) {
fun SearchResultList(
items: List<Searchable>,
modifier: Modifier = Modifier
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp)
modifier = modifier
) {
for (item in items) {
key(item.key) {

View File

@ -21,9 +21,16 @@ fun ColumnScope.ContactResults() {
AnimatedVisibility(contacts.isNotEmpty()) {
LauncherCard(
modifier = Modifier.padding(bottom = 8.dp).fillMaxWidth()
modifier = Modifier
.padding(bottom = 8.dp)
.fillMaxWidth()
) {
SearchResultList(items = contacts)
SearchResultList(
items = contacts,
modifier = Modifier
.fillMaxWidth()
.padding(12.dp)
)
}
}
}

View File

@ -21,9 +21,16 @@ fun ColumnScope.FileResults() {
AnimatedVisibility(files.isNotEmpty()) {
LauncherCard(
modifier = Modifier.padding(bottom = 8.dp).fillMaxWidth()
modifier = Modifier
.padding(bottom = 8.dp)
.fillMaxWidth()
) {
SearchResultList(items = files)
SearchResultList(
items = files,
modifier = Modifier
.fillMaxWidth()
.padding(12.dp)
)
}
}
}

View File

@ -0,0 +1,182 @@
package de.mm20.launcher2.ui.launcher.widgets.calendar
import android.content.Context
import android.text.format.DateUtils
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.InnerCard
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.launcher.search.common.list.SearchResultList
import de.mm20.launcher2.ui.pluralResource
import java.time.LocalDate
import java.time.ZoneId
@Composable
fun CalendarWidget() {
val viewModel: CalendarWidgetVM = viewModel()
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(null) {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel.onActive()
}
}
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 4.dp)
) {
IconButton(onClick = { viewModel.previousDay() }) {
Icon(imageVector = Icons.Rounded.ChevronLeft, contentDescription = null)
}
Box(
modifier = Modifier.weight(1f),
contentAlignment = Alignment.Center
) {
val selectedDate by viewModel.selectedDate.observeAsState(LocalDate.now())
var showDropdown by remember { mutableStateOf(false) }
TextButton(onClick = { showDropdown = true }) {
Text(
text = formatDay(context, selectedDate),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Icon(
imageVector = Icons.Rounded.ArrowDropDown,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface,
)
}
DropdownMenu(expanded = showDropdown, onDismissRequest = { showDropdown = false }) {
val availableDates = viewModel.availableDates
for (date in availableDates) {
DropdownMenuItem(text = {
Text(formatDay(context, date))
}, onClick = {
viewModel.selectDate(date)
showDropdown = false
})
}
}
}
IconButton(onClick = { viewModel.nextDay() }) {
Icon(imageVector = Icons.Rounded.ChevronRight, contentDescription = null)
}
IconButton(onClick = { viewModel.createEvent(context) }) {
Icon(imageVector = Icons.Rounded.Add, contentDescription = null)
}
IconButton(onClick = { viewModel.openCalendarApp(context) }) {
Icon(imageVector = Icons.Rounded.OpenInNew, contentDescription = null)
}
}
val events by viewModel.calendarEvents.observeAsState(emptyList())
val hasPermission by viewModel.hasPermission.observeAsState()
Column(
modifier = Modifier
.padding(horizontal = 12.dp)
.padding(bottom = 12.dp)
) {
if (hasPermission == false) {
MissingPermissionBanner(
modifier = Modifier
.fillMaxWidth()
.padding(4.dp),
text = stringResource(R.string.permission_calendar_widget),
onClick = { viewModel.requestCalendarPermission(context as AppCompatActivity) }
)
}
if (events.isEmpty() && hasPermission == true) {
Info(text = stringResource(R.string.calendar_widget_no_events))
}
SearchResultList(
events,
modifier = Modifier
.fillMaxWidth()
)
val runningEvents by viewModel.hiddenPastEvents.observeAsState(0)
if (runningEvents > 0) {
Info(
text = pluralResource(
R.plurals.calendar_widget_running_events,
runningEvents,
runningEvents
),
onClick = {
viewModel.showAllEvents()
}
)
}
val pinnedEvents by viewModel.pinnedCalendarEvents.observeAsState(emptyList())
if (pinnedEvents.size > 0) {
Text(
stringResource(R.string.calendar_widget_pinned_events),
modifier = Modifier.padding(start = 4.dp, end = 4.dp, top = 4.dp),
style = MaterialTheme.typography.titleMedium
)
SearchResultList(
pinnedEvents,
modifier = Modifier
.fillMaxWidth()
)
}
}
}
}
@Composable
private fun Info(
text: String,
onClick: (() -> Unit)? = null,
) {
InnerCard(
modifier = Modifier
.fillMaxWidth()
.padding(4.dp)
) {
Box(
contentAlignment = Alignment.CenterStart,
modifier = Modifier
.padding(12.dp)
.clickable(enabled = onClick != null, onClick = { onClick?.invoke() })
) {
Text(text, style = MaterialTheme.typography.bodySmall)
}
}
}
private fun formatDay(context: Context, day: LocalDate): String {
val today = LocalDate.now()
return when {
today == day -> context.getString(R.string.date_today)
today.plusDays(1) == day -> context.getString(R.string.date_tomorrow)
else -> DateUtils.formatDateTime(
context,
day.atStartOfDay(ZoneId.systemDefault()).toEpochSecond() * 1000,
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_ABBREV_WEEKDAY
)
}
}

View File

@ -4,6 +4,7 @@ import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.provider.CalendarContract
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
@ -47,7 +48,7 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
val startDate =
Instant.ofEpochMilli(it.startTime).atZone(ZoneId.systemDefault()).toLocalDate()
val endDate =
Instant.ofEpochMilli(it.startTime).atZone(ZoneId.systemDefault()).toLocalDate()
Instant.ofEpochMilli(it.endTime).atZone(ZoneId.systemDefault()).toLocalDate()
return@flatMap listOf(
startDate,
endDate
@ -121,7 +122,8 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
val totalCount = events.size
events = events.filter {
it.startTime >= date.atStartOfDay().toEpochSecond(offset) * 1000
it.startTime >= date.atStartOfDay().toEpochSecond(offset) * 1000 ||
it.endTime < date.atStartOfDay().plusDays(1).toEpochSecond(offset) * 1000
}
val hiddenCount = totalCount - events.size
@ -138,4 +140,8 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
upcomingEvents = it
}
}
fun requestCalendarPermission(context: AppCompatActivity) {
permissionsManager.requestPermission(context, PermissionGroup.Calendar)
}
}

View File

@ -1,30 +1,18 @@
package de.mm20.launcher2.ui.legacy.widget
import android.animation.LayoutTransition
import android.content.Context
import android.text.format.DateUtils
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.Menu
import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.search.data.MissingPermission
import de.mm20.launcher2.search.data.Searchable
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.LocalAbsoluteTonalElevation
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.ui.MdcLauncherTheme
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.databinding.ViewCalendarWidgetBinding
import de.mm20.launcher2.ui.launcher.widgets.calendar.CalendarWidgetVM
import de.mm20.launcher2.ui.legacy.data.InformationText
import kotlinx.coroutines.launch
import java.time.LocalDate
import java.time.ZoneId
import de.mm20.launcher2.ui.base.ProvideSettings
import de.mm20.launcher2.ui.launcher.widgets.calendar.CalendarWidget
class CalendarWidget : LauncherWidget {
@ -39,141 +27,25 @@ class CalendarWidget : LauncherWidget {
defStyleRes
)
private fun formatDay(day: LocalDate): String {
val today = LocalDate.now()
return when {
today == day -> context.getString(R.string.date_today)
today.plusDays(1) == day -> context.getString(R.string.date_tomorrow)
else -> DateUtils.formatDateTime(
context,
day.atStartOfDay(ZoneId.systemDefault()).toEpochSecond() * 1000,
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_ABBREV_WEEKDAY
)
}
}
private val binding =
ViewCalendarWidgetBinding.inflate(LayoutInflater.from(context), this, true)
private val viewModel: CalendarWidgetVM by (context as AppCompatActivity).viewModels()
init {
clipToPadding = false
clipChildren = false
lifecycleScope.launch {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel.onActive()
val composeView = ComposeView(context)
composeView.setContent {
MdcLauncherTheme {
ProvideSettings {
// TODO: Temporary solution until parent widget card is rewritten in Compose
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.colorScheme.onSurface,
LocalAbsoluteTonalElevation provides 1.dp
) {
Column {
CalendarWidget()
}
}
}
}
}
addView(composeView)
binding.calendarNewEvent.setOnClickListener {
viewModel.createEvent(context)
}
binding.calendarDate.setOnClickListener {
val menu = PopupMenu(context, binding.calendarDate)
val availableDates = viewModel.availableDates
for ((i, d) in availableDates.withIndex()) {
menu.menu.add(
Menu.NONE,
i,
Menu.NONE,
formatDay(d)
)
}
menu.setOnMenuItemClickListener {
viewModel.selectDate(availableDates[it.itemId])
true
}
menu.show()
}
binding.calendarOpenApp.setOnClickListener {
viewModel.openCalendarApp(context)
}
binding.calendarDateNext.setOnClickListener {
viewModel.nextDay()
}
binding.calendarDatePrev.setOnClickListener {
viewModel.previousDay()
}
val calendarEvents = viewModel.calendarEvents
val pinnedCalendarEvents = viewModel.pinnedCalendarEvents
val hiddenPastEvents = viewModel.hiddenPastEvents
val selectedDate = viewModel.selectedDate
val hasPermission = viewModel.hasPermission
calendarEvents.observe(context as AppCompatActivity) {
updateEventList(it, hiddenPastEvents.value ?: 0, hasPermission.value == false)
}
hasPermission.observe(context as AppCompatActivity) {
updateEventList(
calendarEvents.value ?: emptyList(),
hiddenPastEvents.value ?: 0,
it == false
)
}
pinnedCalendarEvents.observe(context as AppCompatActivity) {
binding.calendarWidgetPinnedList.submitItems(it)
if (it.isEmpty()) {
binding.calendarWidgetPinnedList.visibility = View.GONE
binding.calendarUpcomingEventsTitle.visibility = View.GONE
} else {
binding.calendarWidgetPinnedList.visibility = View.VISIBLE
binding.calendarUpcomingEventsTitle.visibility = View.VISIBLE
}
}
selectedDate.observe(context as AppCompatActivity) {
binding.calendarDate.text = formatDay(it)
}
binding.calendarWidgetRoot.layoutTransition = LayoutTransition().apply {
enableTransitionType(LayoutTransition.CHANGING)
}
}
private fun updateEventList(
events: List<CalendarEvent>,
hiddenPastDayEvents: Int,
missingPermission: Boolean
) {
val items = events.toMutableList<Searchable>()
if (missingPermission) {
items.add(
MissingPermission(
context.getString(R.string.permission_calendar_widget),
PermissionGroup.Calendar
)
)
}
if (events.isEmpty() && !missingPermission) {
items.add(
InformationText(context.getString(R.string.calendar_widget_no_events))
)
}
if (hiddenPastDayEvents > 0) {
items.add(
InformationText(
resources.getQuantityString(
R.plurals.calendar_widget_running_events,
hiddenPastDayEvents,
hiddenPastDayEvents
)
) {
viewModel.showAllEvents()
})
}
binding.calendarWidgetList.submitItems(items)
}