Migrate calendar widget to Compose
This commit is contained in:
parent
386083ede9
commit
ca813f3f70
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user