diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarResults.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarResults.kt index 1074da07..d48c385f 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarResults.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarResults.kt @@ -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) + ) } } } \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/SearchResultList.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/SearchResultList.kt index 0b9daa42..10cf4ff5 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/SearchResultList.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/SearchResultList.kt @@ -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) { +fun SearchResultList( + items: List, + modifier: Modifier = Modifier +) { Column( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp) + modifier = modifier ) { for (item in items) { key(item.key) { diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactResults.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactResults.kt index 343d4f48..ed640c8e 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactResults.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactResults.kt @@ -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) + ) } } } \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileResults.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileResults.kt index e206109c..67ac7486 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileResults.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileResults.kt @@ -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) + ) } } } \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidget.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidget.kt new file mode 100644 index 00000000..91f509fb --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidget.kt @@ -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 + ) + } +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidgetVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidgetVM.kt index 32ebabd7..edbe1d53 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidgetVM.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidgetVM.kt @@ -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) + } } \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/CalendarWidget.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/CalendarWidget.kt index 2801fc30..9ed2d494 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/CalendarWidget.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/CalendarWidget.kt @@ -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, - hiddenPastDayEvents: Int, - missingPermission: Boolean - ) { - val items = events.toMutableList() - - 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) }