diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/BottomSheetDialog.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/BottomSheetDialog.kt index dd1b17a0..e7ce5e66 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/BottomSheetDialog.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/BottomSheetDialog.kt @@ -26,10 +26,13 @@ import androidx.compose.material.FractionalThreshold import androidx.compose.material.SwipeableState import androidx.compose.material.swipeable import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.LocalAbsoluteTonalElevation import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocal +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -133,143 +136,147 @@ fun BottomSheetDialog( } } - Dialog( - properties = DialogProperties( - dismissOnBackPress = dismissOnBackPress(), - dismissOnClickOutside = swipeToDismiss(), - usePlatformDefaultWidth = false, - ), - onDismissRequest = onDismissRequest, + CompositionLocalProvider( + LocalAbsoluteTonalElevation provides 0.dp, ) { - BoxWithConstraints( - modifier = Modifier.fillMaxSize(), - propagateMinConstraints = true, - contentAlignment = Alignment.BottomCenter + Dialog( + properties = DialogProperties( + dismissOnBackPress = dismissOnBackPress(), + dismissOnClickOutside = swipeToDismiss(), + usePlatformDefaultWidth = false, + ), + onDismissRequest = onDismissRequest, ) { - val maxHeightPx = maxHeight.toPixels() - val scrimAlpha by animateFloatAsState( - if (swipeState.targetValue == SwipeState.Dismiss) 0f else 0.32f, - label = "Scrim alpha" - ) + BoxWithConstraints( + modifier = Modifier.fillMaxSize(), + propagateMinConstraints = true, + contentAlignment = Alignment.BottomCenter + ) { + val maxHeightPx = maxHeight.toPixels() + val scrimAlpha by animateFloatAsState( + if (swipeState.targetValue == SwipeState.Dismiss) 0f else 0.32f, + label = "Scrim alpha" + ) - Box(modifier = Modifier - .background(MaterialTheme.colorScheme.scrim.copy(alpha = scrimAlpha)) - .fillMaxSize() - .pointerInput(onDismissRequest, swipeToDismiss) { - detectTapGestures { - if (swipeToDismiss()) { - scope.launch { - swipeState.animateTo(SwipeState.Dismiss) - onDismissRequest() + Box(modifier = Modifier + .background(MaterialTheme.colorScheme.scrim.copy(alpha = scrimAlpha)) + .fillMaxSize() + .pointerInput(onDismissRequest, swipeToDismiss) { + detectTapGestures { + if (swipeToDismiss()) { + scope.launch { + swipeState.animateTo(SwipeState.Dismiss) + onDismissRequest() + } } } } - } - ) + ) - Column( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(Alignment.Bottom) - .clipToBounds(), - verticalArrangement = Arrangement.Bottom - ) { - var height by remember { - mutableStateOf(maxHeightPx) - } - - LaunchedEffect(null) { - swipeState.animateTo(SwipeState.Peek) - } - - val heightDp = height.toDp() - val peekHeight = (height - maxHeightPx / 2).coerceAtLeast(0f) - val anchors = mutableMapOf( - peekHeight to SwipeState.Peek, - height to SwipeState.Dismiss, - ).also { - if (peekHeight > 0f) { - it[0f] = SwipeState.Full - } - } - Surface( + Column( modifier = Modifier - .nestedScroll(nestedScrollConnection) - .animateContentSize() - .onSizeChanged { - height = it.height.toFloat() - } - .offset { IntOffset(0, swipeState.offset.value.roundToInt()) } - .swipeable( - swipeState, - anchors = anchors, - orientation = Orientation.Vertical, - thresholds = { _, to -> - if (to == SwipeState.Dismiss) { - FixedThreshold(heightDp - 48.dp) - } else { - FractionalThreshold(0.5f) - } - }, - resistance = null - ) .fillMaxWidth() - .weight(1f, false), - shape = MaterialTheme.shapes.extraLarge.copy( - bottomStart = CornerSize(0), - bottomEnd = CornerSize(0), - ), - shadowElevation = 16.dp, + .wrapContentHeight(Alignment.Bottom) + .clipToBounds(), + verticalArrangement = Arrangement.Bottom ) { - Column { - CenterAlignedTopAppBar( - title = title, - actions = actions, - colors = TopAppBarDefaults.centerAlignedTopAppBarColors( - containerColor = Color.Transparent, - ), - ) - Box( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - propagateMinConstraints = true, - contentAlignment = Alignment.Center - ) { - content(PaddingValues(horizontal = 24.dp, vertical = 8.dp)) - } - + var height by remember { + mutableStateOf(maxHeightPx) } - } - if (confirmButton != null || dismissButton != null) { - val elevation by animateDpAsState(if (swipeState.offset.value == 0f) 0.dp else 1.dp) - val alpha by animateFloatAsState(if (swipeState.targetValue == SwipeState.Dismiss) 0f else 1f) + LaunchedEffect(null) { + swipeState.animateTo(SwipeState.Peek) + } + + val heightDp = height.toDp() + val peekHeight = (height - maxHeightPx / 2).coerceAtLeast(0f) + val anchors = mutableMapOf( + peekHeight to SwipeState.Peek, + height to SwipeState.Dismiss, + ).also { + if (peekHeight > 0f) { + it[0f] = SwipeState.Full + } + } Surface( modifier = Modifier - .wrapContentHeight() - .fillMaxWidth(), - tonalElevation = elevation, + .nestedScroll(nestedScrollConnection) + .animateContentSize() + .onSizeChanged { + height = it.height.toFloat() + } + .offset { IntOffset(0, swipeState.offset.value.roundToInt()) } + .swipeable( + swipeState, + anchors = anchors, + orientation = Orientation.Vertical, + thresholds = { _, to -> + if (to == SwipeState.Dismiss) { + FixedThreshold(heightDp - 48.dp) + } else { + FractionalThreshold(0.5f) + } + }, + resistance = null + ) + .fillMaxWidth() + .weight(1f, false), + shape = MaterialTheme.shapes.extraLarge.copy( + bottomStart = CornerSize(0), + bottomEnd = CornerSize(0), + ), + shadowElevation = 16.dp, ) { - Row( + Column { + CenterAlignedTopAppBar( + title = title, + actions = actions, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = Color.Transparent, + ), + ) + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + propagateMinConstraints = true, + contentAlignment = Alignment.Center + ) { + content(PaddingValues(horizontal = 24.dp, vertical = 8.dp)) + } + + } + } + + if (confirmButton != null || dismissButton != null) { + val elevation by animateDpAsState(if (swipeState.offset.value == 0f) 0.dp else 1.dp) + val alpha by animateFloatAsState(if (swipeState.targetValue == SwipeState.Dismiss) 0f else 1f) + Surface( modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - horizontalArrangement = Arrangement.End + .wrapContentHeight() + .fillMaxWidth(), + tonalElevation = elevation, ) { - if (dismissButton != null) { - dismissButton() - } - if (confirmButton != null && dismissButton != null) { - Spacer(modifier = Modifier.width(16.dp)) - } - if (confirmButton != null) { - confirmButton() + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.End + ) { + if (dismissButton != null) { + dismissButton() + } + if (confirmButton != null && dismissButton != null) { + Spacer(modifier = Modifier.width(16.dp)) + } + if (confirmButton != null) { + confirmButton() + } } + } } - } } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/CheckboxPreference.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/CheckboxPreference.kt new file mode 100644 index 00000000..79a3fd83 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/CheckboxPreference.kt @@ -0,0 +1,32 @@ +package de.mm20.launcher2.ui.component.preferences + +import androidx.compose.material3.Checkbox +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector + +@Composable +fun CheckboxPreference( + title: String, + icon: ImageVector? = null, + iconPadding: Boolean = true, + summary: String? = null, + value: Boolean, + onValueChanged: (Boolean) -> Unit, + enabled: Boolean = true +) { + Preference( + title = title, + icon = icon, + iconPadding = iconPadding, + summary = summary, + enabled = enabled, + onClick = { + onValueChanged(!value) + }, + controls = { + Checkbox( + enabled = enabled, checked = value, onCheckedChange = onValueChanged, + ) + } + ) +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/SwitchPreference.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/SwitchPreference.kt index 6d7884ec..4b1c3dd2 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/SwitchPreference.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/SwitchPreference.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.graphics.vector.ImageVector fun SwitchPreference( title: String, icon: ImageVector? = null, + iconPadding: Boolean = true, summary: String? = null, value: Boolean, onValueChanged: (Boolean) -> Unit, @@ -16,6 +17,7 @@ fun SwitchPreference( Preference( title = title, icon = icon, + iconPadding = iconPadding, summary = summary, enabled = enabled, onClick = { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/ConfigureWidgetSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/ConfigureWidgetSheet.kt new file mode 100644 index 00000000..de96a8c9 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/ConfigureWidgetSheet.kt @@ -0,0 +1,536 @@ +package de.mm20.launcher2.ui.launcher.sheets + +import android.app.Activity +import android.appwidget.AppWidgetHost +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo +import android.content.Intent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Build +import androidx.compose.material.icons.rounded.Error +import androidx.compose.material.icons.rounded.OpenInNew +import androidx.compose.material.icons.rounded.UnfoldMore +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import de.mm20.launcher2.calendar.CalendarRepository +import de.mm20.launcher2.ktx.isAtLeastApiLevel +import de.mm20.launcher2.permissions.PermissionGroup +import de.mm20.launcher2.permissions.PermissionsManager +import de.mm20.launcher2.search.data.UserCalendar +import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.component.BottomSheetDialog +import de.mm20.launcher2.ui.component.LargeMessage +import de.mm20.launcher2.ui.component.MissingPermissionBanner +import de.mm20.launcher2.ui.component.preferences.CheckboxPreference +import de.mm20.launcher2.ui.component.preferences.SwitchPreference +import de.mm20.launcher2.ui.launcher.widgets.external.ExternalWidget +import de.mm20.launcher2.ui.settings.SettingsActivity +import de.mm20.launcher2.widgets.AppWidget +import de.mm20.launcher2.widgets.CalendarWidget +import de.mm20.launcher2.widgets.FavoritesWidget +import de.mm20.launcher2.widgets.MusicWidget +import de.mm20.launcher2.widgets.WeatherWidget +import de.mm20.launcher2.widgets.Widget +import org.koin.androidx.compose.get +import kotlin.math.roundToInt + +@Composable +fun ConfigureWidgetSheet( + appWidgetHost: AppWidgetHost, + widget: Widget, + onWidgetUpdated: (Widget) -> Unit, + onDismiss: () -> Unit, +) { + BottomSheetDialog(onDismissRequest = onDismiss, + title = { + Box( + modifier = Modifier + .width(32.dp) + .height(4.dp) + .clip(MaterialTheme.shapes.small) + .background(MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f)) + ) + } + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = if (widget is AppWidget) 8.dp else 16.dp) + .verticalScroll(rememberScrollState()) + .padding(bottom = 8.dp) + ) { + when (widget) { + is WeatherWidget -> ConfigureWeatherWidget(widget, onWidgetUpdated) + is AppWidget -> ConfigureAppWidget(appWidgetHost, widget, onWidgetUpdated) + is CalendarWidget -> ConfigureCalendarWidget(widget, onWidgetUpdated) + is FavoritesWidget -> ConfigureFavoritesWidget(widget, onWidgetUpdated) + is MusicWidget -> ConfigureMusicWidget() + } + } + + } +} + +@Composable +fun ColumnScope.ConfigureWeatherWidget( + widget: WeatherWidget, + onWidgetUpdated: (WeatherWidget) -> Unit, +) { + val context = LocalContext.current + + OutlinedCard { + Column( + modifier = Modifier.fillMaxWidth() + ) { + SwitchPreference( + title = stringResource(R.string.widget_config_weather_compact), + iconPadding = false, + value = !widget.config.showForecast, + onValueChanged = { + onWidgetUpdated(widget.copy(config = widget.config.copy(showForecast = !it))) + } + ) + } + } + TextButton( + modifier = Modifier + .padding(top = 8.dp) + .align(Alignment.End), + contentPadding = PaddingValues( + end = 16.dp, + top = 8.dp, + start = 24.dp, + bottom = 8.dp, + ), + onClick = { + context.startActivity(Intent( + context, + SettingsActivity::class.java + ).apply { + putExtra( + SettingsActivity.EXTRA_ROUTE, + SettingsActivity.ROUTE_WEATHER_INTEGRATION + ) + }) + }) { + Text(stringResource(R.string.widget_config_weather_integration_settings)) + Icon( + modifier = Modifier + .padding(start = ButtonDefaults.IconSpacing) + .size(ButtonDefaults.IconSize), + imageVector = Icons.Rounded.OpenInNew, contentDescription = null + ) + } +} + +@Composable +fun ColumnScope.ConfigureFavoritesWidget( + widget: FavoritesWidget, + onWidgetUpdated: (FavoritesWidget) -> Unit, +) { + val bottomSheetManager = LocalBottomSheetManager.current + OutlinedCard { + Column( + modifier = Modifier.fillMaxWidth() + ) { + SwitchPreference( + title = stringResource(R.string.preference_edit_button), + iconPadding = false, + value = widget.config.editButton, + onValueChanged = { + onWidgetUpdated(widget.copy(config = widget.config.copy(editButton = it))) + } + ) + } + } + TextButton( + modifier = Modifier + .padding(top = 8.dp) + .align(Alignment.End), + contentPadding = PaddingValues( + end = 16.dp, + top = 8.dp, + start = 24.dp, + bottom = 8.dp, + ), + onClick = { + bottomSheetManager.showEditFavoritesSheet() + }) { + Text(stringResource(R.string.menu_item_edit_favs)) + Icon( + modifier = Modifier + .padding(start = ButtonDefaults.IconSpacing) + .size(ButtonDefaults.IconSize), + imageVector = Icons.Rounded.OpenInNew, contentDescription = null + ) + } +} + +@Composable +fun ColumnScope.ConfigureMusicWidget( + +) { + val context = LocalContext.current + + TextButton( + modifier = Modifier + .align(Alignment.CenterHorizontally), + contentPadding = PaddingValues( + end = 16.dp, + top = 8.dp, + start = 24.dp, + bottom = 8.dp, + ), + onClick = { + context.startActivity(Intent( + context, + SettingsActivity::class.java + ).apply { + putExtra( + SettingsActivity.EXTRA_ROUTE, + SettingsActivity.ROUTE_MEDIA_INTEGRATION, + ) + }) + }) { + Text(stringResource(R.string.widget_config_music_integration_settings)) + Icon( + modifier = Modifier + .padding(start = ButtonDefaults.IconSpacing) + .size(ButtonDefaults.IconSize), + imageVector = Icons.Rounded.OpenInNew, contentDescription = null + ) + } +} + +@Composable +fun ColumnScope.ConfigureAppWidget( + appWidgetHost: AppWidgetHost, + widget: AppWidget, + onWidgetUpdated: (Widget) -> Unit, +) { + val context = LocalContext.current + val widgetInfo = remember(widget.config.widgetId) { + AppWidgetManager.getInstance(context).getAppWidgetInfo(widget.config.widgetId) + } + + if (widgetInfo == null) { + var replaceWidget by rememberSaveable { + mutableStateOf(false) + } + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 24.dp), + contentAlignment = Alignment.Center + ) { + LargeMessage( + icon = Icons.Rounded.Error, + text = stringResource(id = R.string.app_widget_loading_failed) + ) + } + OutlinedButton( + modifier = Modifier + .padding(vertical = 24.dp) + .align(Alignment.CenterHorizontally), + contentPadding = ButtonDefaults.ButtonWithIconContentPadding, + onClick = { replaceWidget = true }) { + Icon( + Icons.Rounded.Build, + null, + modifier = Modifier + .padding(end = ButtonDefaults.IconSpacing) + .size(ButtonDefaults.IconSize) + ) + Text(stringResource(R.string.widget_action_replace)) + } + if (replaceWidget) { + WidgetPickerSheet( + onDismiss = { replaceWidget = false }, + onWidgetSelected = { + val updatedWidget = when (it) { + is AppWidget -> widget.copy( + config = widget.config.copy( + widgetId = it.config.widgetId + ) + ) + + is WeatherWidget -> it.copy(id = widget.id) + is MusicWidget -> it.copy(id = widget.id) + is CalendarWidget -> it.copy(id = widget.id) + is FavoritesWidget -> it.copy(id = widget.id) + } + onWidgetUpdated(updatedWidget) + replaceWidget = false + } + ) + } + return + } + + var dragDelta by remember { mutableStateOf(0) } + Column { + Box( + modifier = Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .background(MaterialTheme.colorScheme.surfaceVariant) + ) { + ExternalWidget( + appWidgetHost = appWidgetHost, + widgetInfo = widgetInfo, + widgetId = widget.config.widgetId, + modifier = Modifier.fillMaxWidth(), + height = widget.config.height + dragDelta, + borderless = widget.config.borderless, + ) + } + val density = LocalDensity.current + val draggableState = rememberDraggableState { + dragDelta = (dragDelta + it / density.density).roundToInt() + .coerceIn( + -widget.config.height + 1, + 500 - widget.config.height + ) + } + Row( + modifier = Modifier + .padding(top = 8.dp, bottom = 16.dp) + .padding(horizontal = 8.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .padding(end = 16.dp) + .clip(MaterialTheme.shapes.small) + .background(MaterialTheme.colorScheme.primaryContainer) + .height(36.dp) + .width(48.dp) + .draggable( + state = draggableState, + orientation = Orientation.Vertical, + startDragImmediately = true, + onDragStopped = { + onWidgetUpdated( + widget.copy( + config = widget.config.copy( + height = widget.config.height + dragDelta + ) + ) + ) + dragDelta = 0 + } + ), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Rounded.UnfoldMore, + contentDescription = null, + ) + } + var textFieldValue by remember(widget.config.height) { mutableStateOf(widget.config.height.toString()) } + OutlinedTextField( + modifier = Modifier + .weight(1f) + .padding(bottom = 8.dp), + value = (widget.config.height + dragDelta).toString(), + onValueChange = { + val intValue = it.toIntOrNull() + if (intValue == null) textFieldValue = "" + else if (intValue in 1..500) { + onWidgetUpdated( + widget.copy( + config = widget.config.copy( + height = intValue + ) + ) + ) + textFieldValue = intValue.toString() + } + }, + label = { Text(stringResource(R.string.widget_config_appwidget_height)) }, + suffix = { Text("dp") }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done + ), + ) + } + } + + Column( + modifier = Modifier.padding(horizontal = 8.dp) + ) { + OutlinedCard { + Column( + modifier = Modifier.fillMaxWidth() + ) { + SwitchPreference( + title = stringResource(R.string.widget_config_appwidget_borderless), + iconPadding = false, + value = widget.config.borderless, + onValueChanged = { + onWidgetUpdated(widget.copy(config = widget.config.copy(borderless = it))) + } + ) + } + } + if (isAtLeastApiLevel(28) && widgetInfo.widgetFeatures and AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE != 0) { + TextButton( + modifier = Modifier + .padding(top = 8.dp) + .align(Alignment.End), + contentPadding = PaddingValues( + end = 16.dp, + top = 8.dp, + start = 24.dp, + bottom = 8.dp, + ), + onClick = { + appWidgetHost.startAppWidgetConfigureActivityForResult( + context as Activity, + widget.config.widgetId, + 0, + 0, + null + ) + }) { + Text(stringResource(id = R.string.widget_config_appwidget_configure)) + Icon( + modifier = Modifier + .padding(start = ButtonDefaults.IconSpacing) + .size(ButtonDefaults.IconSize), + imageVector = Icons.Rounded.OpenInNew, contentDescription = null + ) + } + } + } +} + +@Composable +fun ColumnScope.ConfigureCalendarWidget( + widget: CalendarWidget, + onWidgetUpdated: (CalendarWidget) -> Unit +) { + val calendarRepository: CalendarRepository = get() + val permissionsManager: PermissionsManager = get() + var calendars by remember { mutableStateOf(emptyList()) } + var ready by remember { mutableStateOf(false) } + + val hasPermission by remember { + permissionsManager.hasPermission(PermissionGroup.Calendar) + }.collectAsState(true) + + LaunchedEffect(hasPermission) { + calendars = calendarRepository.getCalendars().sortedBy { it.name } + ready = true + } + + OutlinedCard { + Column( + modifier = Modifier.fillMaxWidth() + ) { + SwitchPreference( + title = stringResource(R.string.preference_calendar_hide_allday), + iconPadding = false, + value = !widget.config.allDayEvents, + onValueChanged = { + onWidgetUpdated(widget.copy(config = widget.config.copy(allDayEvents = !it))) + } + ) + } + } + Text( + modifier = Modifier.padding(top = 16.dp, bottom = 4.dp), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.secondary, + text = stringResource(R.string.preference_calendar_calendars) + ) + val context = LocalLifecycleOwner.current as AppCompatActivity + if (calendars.isNotEmpty()) { + OutlinedCard { + Column( + modifier = Modifier.fillMaxWidth() + ) { + for ((i, calendar) in calendars.withIndex()) { + if (i > 0) Divider() + CheckboxPreference( + title = calendar.name, + summary = calendar.owner, + iconPadding = false, + value = !widget.config.excludedCalendarIds.contains(calendar.id), + onValueChanged = { + onWidgetUpdated( + widget.copy( + config = widget.config.copy( + excludedCalendarIds = if (it) { + widget.config.excludedCalendarIds - calendar.id + } else { + widget.config.excludedCalendarIds + calendar.id + } + ) + ) + ) + } + ) + } + } + } + } else if (!hasPermission) { + MissingPermissionBanner( + modifier = Modifier.padding(8.dp), + text = stringResource(R.string.missing_permission_calendar_widget_settings), + onClick = { permissionsManager.requestPermission(context, PermissionGroup.Calendar) }, + ) + } else if (ready) { + Text( + modifier = Modifier.padding(top = 8.dp, bottom = 16.dp), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + text = "No calendars found" + ) + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheetManager.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheetManager.kt index 564eaad2..91f342fd 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheetManager.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheetManager.kt @@ -15,7 +15,6 @@ class LauncherBottomSheetManager(registryOwner: SavedStateRegistryOwner) : val customizeSearchableSheetShown = mutableStateOf(null) val editFavoritesSheetShown = mutableStateOf(false) val hiddenItemsSheetShown = mutableStateOf(false) - val widgetPickerSheetShown = mutableStateOf(false) init { registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event -> @@ -28,7 +27,6 @@ class LauncherBottomSheetManager(registryOwner: SavedStateRegistryOwner) : editFavoritesSheetShown.value = state?.getBoolean(FAVORITES) ?: false hiddenItemsSheetShown.value = state?.getBoolean(HIDDEN) ?: false - widgetPickerSheetShown.value = state?.getBoolean(WIDGETS) ?: false } }) } @@ -37,7 +35,6 @@ class LauncherBottomSheetManager(registryOwner: SavedStateRegistryOwner) : return bundleOf( FAVORITES to editFavoritesSheetShown.value, HIDDEN to hiddenItemsSheetShown.value, - WIDGETS to widgetPickerSheetShown.value, ) } @@ -65,14 +62,6 @@ class LauncherBottomSheetManager(registryOwner: SavedStateRegistryOwner) : hiddenItemsSheetShown.value = false } - fun showWidgetPickerSheet() { - widgetPickerSheetShown.value = true - } - - fun dismissWidgetPickerSheet() { - widgetPickerSheetShown.value = false - } - companion object { private const val PROVIDER = "bottom_sheet_manager" private const val FAVORITES = "favorites" diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheets.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheets.kt index eebfab76..b11117c8 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheets.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheets.kt @@ -13,9 +13,4 @@ fun LauncherBottomSheets() { if (bottomSheetManager.editFavoritesSheetShown.value) { EditFavoritesSheet(onDismiss = { bottomSheetManager.dismissEditFavoritesSheet() }) } - if (bottomSheetManager.widgetPickerSheetShown.value) { - WidgetPickerSheet( - onDismiss = { bottomSheetManager.dismissWidgetPickerSheet() } - ) - } } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheet.kt index 562b1462..91eb8068 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheet.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheet.kt @@ -199,7 +199,6 @@ private class BindAndConfigureAppWidgetContract( height = widgetProviderInfo.minHeight, widgetId = widgetId, ), - widgetProviderInfo = widgetProviderInfo, ) } } @@ -210,6 +209,7 @@ private class BindAndConfigureAppWidgetContract( @Composable fun WidgetPickerSheet( + onWidgetSelected: (Widget) -> Unit, onDismiss: () -> Unit ) { val context = LocalContext.current @@ -219,7 +219,7 @@ fun WidgetPickerSheet( val bindAppWidgetStarter = rememberLauncherForActivityResult(BindAndConfigureAppWidgetContract()) { if (it != null) { - viewModel.pickWidget(it) + onWidgetSelected(it) onDismiss() } } @@ -290,7 +290,7 @@ fun WidgetPickerSheet( FavoritesWidget.Type -> FavoritesWidget(id) else -> return@OutlinedCard } - viewModel.pickWidget(widget) + onWidgetSelected(widget) onDismiss() }) { Row( diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheetVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheetVM.kt index 892e2fe0..b003b8b2 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheetVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheetVM.kt @@ -110,11 +110,6 @@ class WidgetPickerSheetVM( val expandedGroup = mutableStateOf(null) - fun pickWidget(widget: Widget) { - val position = enabledWidgets.value.size - widgetsService.addWidget(widget, position) - } - fun toggleGroup(group: String) { expandedGroup.value = if (expandedGroup.value == group) null else group } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt index e323addd..29442760 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt @@ -24,6 +24,8 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onPlaced @@ -39,6 +41,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.ktx.animateTo import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager +import de.mm20.launcher2.ui.launcher.sheets.WidgetPickerSheet import de.mm20.launcher2.widgets.AppWidget import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.launch @@ -56,6 +59,8 @@ fun WidgetColumn( val lifecycleOwner = LocalLifecycleOwner.current val widgetHost = remember { AppWidgetHost(context.applicationContext, 44203) } + var addNewWidget by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(null) { lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { widgetHost.startListening() @@ -166,10 +171,20 @@ fun WidgetColumn( if (!editMode) { onEditModeChange(true) } else { - bottomSheetManager.showWidgetPickerSheet() + addNewWidget = true } }) } } + + if (addNewWidget) { + WidgetPickerSheet( + onDismiss = { addNewWidget = false }, + onWidgetSelected = { + viewModel.addWidget(it) + addNewWidget = false + } + ) + } } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetItem.kt index 34216725..383efef2 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetItem.kt @@ -1,36 +1,57 @@ package de.mm20.launcher2.ui.launcher.widgets -import android.app.Activity import android.appwidget.AppWidgetHost -import android.util.Log +import android.appwidget.AppWidgetManager import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateDpAsState -import androidx.compose.foundation.gestures.* -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.gestures.DraggableState +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.* +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material.icons.rounded.DragIndicator +import androidx.compose.material.icons.rounded.Tune +import androidx.compose.material.icons.rounded.Warning +import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.component.Banner import de.mm20.launcher2.ui.component.LauncherCard +import de.mm20.launcher2.ui.launcher.sheets.ConfigureWidgetSheet +import de.mm20.launcher2.ui.launcher.sheets.WidgetPickerSheet import de.mm20.launcher2.ui.launcher.widgets.calendar.CalendarWidget import de.mm20.launcher2.ui.launcher.widgets.external.ExternalWidget import de.mm20.launcher2.ui.launcher.widgets.favorites.FavoritesWidget import de.mm20.launcher2.ui.launcher.widgets.music.MusicWidget import de.mm20.launcher2.ui.launcher.widgets.weather.WeatherWidget -import de.mm20.launcher2.widgets.* -import kotlin.math.roundToInt +import de.mm20.launcher2.widgets.AppWidget +import de.mm20.launcher2.widgets.CalendarWidget +import de.mm20.launcher2.widgets.FavoritesWidget +import de.mm20.launcher2.widgets.MusicWidget +import de.mm20.launcher2.widgets.WeatherWidget +import de.mm20.launcher2.widgets.Widget @Composable fun WidgetItem( @@ -44,11 +65,16 @@ fun WidgetItem( onDragStopped: () -> Unit = {} ) { val context = LocalContext.current - var resizeMode by remember(editMode) { mutableStateOf(false) } + + var configure by rememberSaveable { mutableStateOf(false) } var isDragged by remember { mutableStateOf(false) } val elevation by animateDpAsState(if (isDragged) 8.dp else 2.dp) + val appWidget = if (widget is AppWidget) remember(widget.config.widgetId) { + AppWidgetManager.getInstance(context).getAppWidgetInfo(widget.config.widgetId) + } else null + LauncherCard( modifier = modifier.zIndex(if (isDragged) 1f else 0f), elevation = elevation @@ -76,7 +102,18 @@ fun WidgetItem( ) ) Text( - text = remember(widget) { widget.loadLabel(context) }, + text = when (widget) { + is WeatherWidget -> stringResource(R.string.widget_name_weather) + is MusicWidget -> stringResource(R.string.widget_name_music) + is CalendarWidget -> stringResource(R.string.widget_name_calendar) + is FavoritesWidget -> stringResource(R.string.widget_name_favorites) + is AppWidget -> remember(widget.config.widgetId) { + appWidget?.loadLabel( + context.packageManager + ) + } + ?: stringResource(R.string.widget_name_unknown) + }, style = MaterialTheme.typography.titleMedium, modifier = Modifier .weight(1f) @@ -84,23 +121,13 @@ fun WidgetItem( overflow = TextOverflow.Ellipsis, maxLines = 1 ) - if (widget is AppWidget) { - IconButton(onClick = { resizeMode = !resizeMode }) { - Icon( - imageVector = Icons.Rounded.Edit, - contentDescription = stringResource(R.string.widget_action_adjust_height) - ) - } - } - if (widget.isConfigurable) { - IconButton({ - widget.configure(context as Activity, appWidgetHost) - }) { - Icon( - imageVector = Icons.Rounded.Settings, - contentDescription = stringResource(R.string.settings) - ) - } + IconButton(onClick = { + configure = true + }) { + Icon( + imageVector = Icons.Rounded.Tune, + contentDescription = stringResource(R.string.settings) + ) } IconButton(onClick = { onWidgetRemove() }) { Icon( @@ -110,61 +137,89 @@ fun WidgetItem( } } } - AnimatedVisibility(!editMode || resizeMode) { + AnimatedVisibility(!editMode) { when (widget) { is WeatherWidget -> { - WeatherWidget() + WeatherWidget(widget) } + is MusicWidget -> { MusicWidget() } + is CalendarWidget -> { - CalendarWidget() + CalendarWidget(widget) } + is FavoritesWidget -> { - FavoritesWidget() + FavoritesWidget(widget) } + is AppWidget -> { - var dragDelta by remember { mutableStateOf(0) } - Column { + val widgetInfo = remember(widget.config.widgetId) { + AppWidgetManager.getInstance(context) + .getAppWidgetInfo(widget.config.widgetId) + } + if (widgetInfo == null) { + var replaceWidget by rememberSaveable { + mutableStateOf(false) + } + Banner( + modifier = Modifier.padding(16.dp), + text = stringResource(R.string.app_widget_loading_failed), + icon = Icons.Rounded.Warning, + secondaryAction = { + OutlinedButton(onClick = onWidgetRemove) { + Text(stringResource(R.string.widget_action_remove)) + } + }, + primaryAction = { + Button(onClick = { replaceWidget = true }) { + Text(stringResource(R.string.widget_action_replace)) + } + } + ) + if (replaceWidget) { + WidgetPickerSheet( + onDismiss = { replaceWidget = false }, + onWidgetSelected = { + val updatedWidget = when (it) { + is AppWidget -> widget.copy( + config = widget.config.copy( + widgetId = it.config.widgetId + ) + ) + is WeatherWidget -> it.copy(id = widget.id) + is MusicWidget -> it.copy(id = widget.id) + is CalendarWidget -> it.copy(id = widget.id) + is FavoritesWidget -> it.copy(id = widget.id) + } + onWidgetUpdate(updatedWidget) + replaceWidget = false + } + ) + } + } else { ExternalWidget( appWidgetHost = appWidgetHost, widgetId = widget.config.widgetId, + widgetInfo = widgetInfo, modifier = Modifier.fillMaxWidth(), - height = widget.config.height + dragDelta, + height = widget.config.height, + borderless = widget.config.borderless, ) - if (resizeMode) { - val density = LocalDensity.current - val drgStt = rememberDraggableState { - dragDelta += (it / density.density).roundToInt() - } - Icon( - imageVector = Icons.Rounded.DragHandle, - contentDescription = null, - modifier = Modifier - .padding(top = 12.dp) - .requiredHeight(24.dp) - .fillMaxWidth() - .draggable( - state = drgStt, - orientation = Orientation.Vertical, - startDragImmediately = true, - onDragStopped = { - onWidgetUpdate(widget.copy( - config = widget.config.copy( - height = widget.config.height + dragDelta - ) - )) - dragDelta = 0 - } - ) - ) - } - } } } } } } -} \ No newline at end of file + if (configure) { + ConfigureWidgetSheet( + appWidgetHost = appWidgetHost, + widget = widget, + onWidgetUpdated = onWidgetUpdate, + onDismiss = { configure = false }, + ) + } +} diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsVM.kt index 9713d487..31d21061 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsVM.kt @@ -19,6 +19,11 @@ class WidgetsVM : ViewModel(), KoinComponent { val widgets = widgetRepository.get().asLiveData() + fun addWidget(widget: Widget) { + val widgets = widgets.value?.toMutableList() ?: return + widgets.add(widget) + widgetRepository.set(widgets) + } fun removeWidget(widget: Widget) { widgetRepository.delete(widget) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidget.kt index 35bb4927..9abe12bc 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidget.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidget.kt @@ -27,11 +27,14 @@ 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.widgets.CalendarWidget import java.time.LocalDate import java.time.ZoneId @Composable -fun CalendarWidget() { +fun CalendarWidget( + widget: CalendarWidget, +) { val viewModel: CalendarWidgetVM = viewModel() val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current @@ -42,6 +45,10 @@ fun CalendarWidget() { } } + LaunchedEffect(widget) { + viewModel.updateWidget(widget) + } + Column { Row( verticalAlignment = Alignment.CenterVertically, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidgetVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidgetVM.kt index e6578ef7..496255ae 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidgetVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidgetVM.kt @@ -17,6 +17,9 @@ import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.search.data.CalendarEvent import de.mm20.launcher2.services.favorites.FavoritesService +import de.mm20.launcher2.widgets.CalendarWidget +import de.mm20.launcher2.widgets.CalendarWidgetConfig +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.map import org.koin.core.component.KoinComponent @@ -30,11 +33,12 @@ import kotlin.math.max class CalendarWidgetVM : ViewModel(), KoinComponent { - private val dataStore: LauncherDataStore by inject() private val calendarRepository: CalendarRepository by inject() private val favoritesService: FavoritesService by inject() private val searchableRepository: SearchableRepository by inject() + private val widgetConfig = MutableStateFlow(CalendarWidgetConfig()) + val calendarEvents = MutableLiveData>(emptyList()) val pinnedCalendarEvents = favoritesService.getFavorites( @@ -53,6 +57,10 @@ class CalendarWidgetVM : ViewModel(), KoinComponent { val selectedDate = MutableLiveData(LocalDate.now()) + fun updateWidget(widget: CalendarWidget) { + widgetConfig.value = widget.config + } + private var upcomingEvents: List = emptyList() set(value) { field = value @@ -155,10 +163,10 @@ class CalendarWidgetVM : ViewModel(), KoinComponent { suspend fun onActive() { selectDate(LocalDate.now()) - dataStore.data.map { it.calendarWidget }.collectLatest { settings -> + widgetConfig.collectLatest { config -> calendarRepository.getUpcomingEvents( - excludeAllDayEvents = settings.hideAlldayEvents, - excludeCalendars = settings.excludeCalendarsList + excludeAllDayEvents = !config.allDayEvents, + excludeCalendars = config.excludedCalendarIds, ).collectLatest { events -> searchableRepository.getKeys( includeTypes = listOf(CalendarEvent.Domain), diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/external/ExternalWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/external/ExternalWidget.kt index 1a852e42..a3a8fdac 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/external/ExternalWidget.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/external/ExternalWidget.kt @@ -2,6 +2,7 @@ package de.mm20.launcher2.ui.launcher.widgets.external import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo import android.os.Bundle import android.util.Log import android.view.View @@ -22,20 +23,20 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.view.doOnNextLayout import androidx.core.view.iterator +import androidx.core.view.setPadding import de.mm20.launcher2.ui.ktx.toPixels import kotlin.math.roundToInt @Composable fun ExternalWidget( appWidgetHost: AppWidgetHost, + widgetInfo: AppWidgetProviderInfo, widgetId: Int, height: Int, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + borderless: Boolean = false, ) { - val context = LocalContext.current - val widgetInfo = remember(widgetId) { - AppWidgetManager.getInstance(context).getAppWidgetInfo(widgetId) - } + val padding = if (borderless) 0 else 8.dp.toPixels().roundToInt() BoxWithConstraints { val maxWidth = maxWidth key(widgetId) { @@ -56,6 +57,7 @@ fun ExternalWidget( maxWidth.value.roundToInt(), height, ) + it.setPadding(padding) } ) } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/favorites/FavoritesWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/favorites/FavoritesWidget.kt index 4820b886..7143f8d4 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/favorites/FavoritesWidget.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/favorites/FavoritesWidget.kt @@ -18,14 +18,15 @@ import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.common.FavoritesTagSelector import de.mm20.launcher2.ui.component.Banner import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid +import de.mm20.launcher2.widgets.FavoritesWidget @Composable -fun FavoritesWidget() { +fun FavoritesWidget(widget: FavoritesWidget) { val viewModel: FavoritesWidgetVM = viewModel() val favorites by remember { viewModel.favorites }.collectAsState(emptyList()) val pinnedTags by viewModel.pinnedTags.collectAsState(emptyList()) val selectedTag by viewModel.selectedTag.collectAsState(null) - val favoritesEditButton by viewModel.showEditButton.collectAsState(false) + val favoritesEditButton = widget.config.editButton val tagsExpanded by viewModel.tagsExpanded.collectAsState(false) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt index 32ca44be..b58040e4 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt @@ -33,6 +33,7 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -44,6 +45,7 @@ import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.booleanResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -62,13 +64,14 @@ import de.mm20.launcher2.ui.ktx.blendIntoViewScale import de.mm20.launcher2.ui.locals.LocalCardStyle import de.mm20.launcher2.weather.DailyForecast import de.mm20.launcher2.weather.Forecast +import de.mm20.launcher2.widgets.WeatherWidget import java.text.DateFormat import java.text.DecimalFormat import java.text.SimpleDateFormat import kotlin.math.roundToInt @Composable -fun WeatherWidget() { +fun WeatherWidget(widget: WeatherWidget) { val viewModel: WeatherWidgetWM = viewModel() val context = LocalContext.current @@ -82,9 +85,8 @@ fun WeatherWidget() { val selectedForecast by viewModel.currentForecast.observeAsState() - val imperialUnits by viewModel.imperialUnits.observeAsState(false) - - val compactMode by viewModel.compactMode.observeAsState(false) + val imperialUnits by viewModel.imperialUnits.collectAsState(false) + val compactMode = !widget.config.showForecast var showLocationDialog by remember { mutableStateOf(false) } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidgetWM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidgetWM.kt index 0ebf2e5b..fde9f748 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidgetWM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidgetWM.kt @@ -8,8 +8,10 @@ import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.weather.DailyForecast import de.mm20.launcher2.weather.Forecast import de.mm20.launcher2.weather.WeatherRepository +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -104,9 +106,7 @@ class WeatherWidgetWM : ViewModel(), KoinComponent { } val autoLocation = weatherRepository.autoLocation.asLiveData() - val imperialUnits = dataStore.data.map { it.weather.imperialUnits }.asLiveData() - - val compactMode = dataStore.data.map { it.weather.compactMode }.asLiveData() + val imperialUnits = dataStore.data.map { it.weather.imperialUnits } fun selectDay(index: Int) { selectedDayIndex = min(index, forecasts.lastIndex) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt index de354a4f..0c8ede18 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt @@ -27,7 +27,6 @@ import de.mm20.launcher2.ui.settings.integrations.IntegrationsSettingsScreen import de.mm20.launcher2.ui.settings.appearance.AppearanceSettingsScreen import de.mm20.launcher2.ui.settings.backup.BackupSettingsScreen import de.mm20.launcher2.ui.settings.buildinfo.BuildInfoSettingsScreen -import de.mm20.launcher2.ui.settings.calendarwidget.CalendarWidgetSettingsScreen import de.mm20.launcher2.ui.settings.cards.CardsSettingsScreen import de.mm20.launcher2.ui.settings.clockwidget.ClockWidgetSettingsScreen import de.mm20.launcher2.ui.settings.colorscheme.ColorSchemeSettingsScreen @@ -139,15 +138,12 @@ class SettingsActivity : BaseActivity() { composable("settings/search/tags") { TagsSettingsScreen() } - composable("settings/integrations/weather") { + composable(ROUTE_WEATHER_INTEGRATION) { WeatherIntegrationSettingsScreen() } - composable("settings/integrations/media") { + composable(ROUTE_MEDIA_INTEGRATION) { MediaIntegrationSettingsScreen() } - composable("settings/widgets/calendar") { - CalendarWidgetSettingsScreen() - } composable("settings/homescreen/clock") { ClockWidgetSettingsScreen() } @@ -214,5 +210,7 @@ class SettingsActivity : BaseActivity() { companion object { const val EXTRA_ROUTE = "de.mm20.launcher2.settings.ROUTE" + const val ROUTE_WEATHER_INTEGRATION = "settings/integrations/weather" + const val ROUTE_MEDIA_INTEGRATION = "settings/integrations/media" } } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/calendarwidget/CalendarWidgetSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/calendarwidget/CalendarWidgetSettingsScreen.kt deleted file mode 100644 index 59181dcc..00000000 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/calendarwidget/CalendarWidgetSettingsScreen.kt +++ /dev/null @@ -1,173 +0,0 @@ -package de.mm20.launcher2.ui.settings.calendarwidget - -import androidx.appcompat.app.AppCompatActivity -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.pluralStringResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.lifecycle.viewmodel.compose.viewModel -import de.mm20.launcher2.search.data.UserCalendar -import de.mm20.launcher2.ui.R -import de.mm20.launcher2.ui.component.MissingPermissionBanner -import de.mm20.launcher2.ui.component.preferences.Preference -import de.mm20.launcher2.ui.component.preferences.PreferenceCategory -import de.mm20.launcher2.ui.component.preferences.PreferenceScreen -import de.mm20.launcher2.ui.component.preferences.SwitchPreference - -@Composable -fun CalendarWidgetSettingsScreen() { - val viewModel: CalendarWidgetSettingsScreenVM = viewModel() - val context = LocalContext.current - PreferenceScreen( - title = stringResource(R.string.preference_screen_calendarwidget), - helpUrl = "https://kvaesitso.mm20.de/docs/user-guide/widgets/calendar-widget" - ) { - item { - val excludeAllDayEvents by viewModel.excludeAllDayEvents.observeAsState() - PreferenceCategory { - val hasPermission by viewModel.hasCalendarPermission.observeAsState() - AnimatedVisibility(hasPermission == false) { - MissingPermissionBanner( - modifier = Modifier.padding(16.dp), - text = stringResource(R.string.missing_permission_calendar_widget_settings), - onClick = { - viewModel.requestPermission(context as AppCompatActivity) - } - ) - } - SwitchPreference( - title = stringResource(R.string.preference_calendar_hide_allday), - value = excludeAllDayEvents == true, - onValueChanged = { - viewModel.setExcludeAllDayEvents(it) - } - ) - val calendars by viewModel.calendars.observeAsState(emptyList()) - val unselectedCalendars by viewModel.unselectedCalendars.observeAsState(emptyList()) - ExcludedCalendarsPreference( - calendars = calendars, - value = unselectedCalendars, - onValueChanged = { - viewModel.setUnselectedCalendars(it) - } - ) - } - } - } -} - -@Composable -fun ExcludedCalendarsPreference( - calendars: List, - value: List, - onValueChanged: (List) -> Unit -) { - var showDialog by remember { mutableStateOf(false) } - Preference( - title = stringResource(R.string.preference_calendar_calendars), - summary = pluralStringResource( - R.plurals.preference_calendar_calendars_summary, - count = calendars.size - value.size, - calendars.size - value.size - ), - onClick = { - showDialog = true - } - ) - if (showDialog) { - Dialog( - onDismissRequest = { showDialog = false }, - ) { - Surface( - modifier = Modifier - .fillMaxWidth() - .heightIn(max = 400.dp), - shape = MaterialTheme.shapes.extraLarge, - tonalElevation = 16.dp, - shadowElevation = 16.dp, - ) { - Column { - Text( - text = stringResource(R.string.preference_calendar_calendars), - style = MaterialTheme.typography.titleLarge, - modifier = Modifier - .fillMaxWidth() - .padding( - start = 24.dp, end = 24.dp, top = 16.dp, bottom = 8.dp - ) - ) - LazyColumn( - modifier = Modifier - .weight(1f) - .padding(horizontal = 16.dp) - ) { - items(calendars) { c -> - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .clickable { - if (value.contains(c.id)) { - onValueChanged( - value.filter { it != c.id } - ) - } else { - onValueChanged( - value + c.id - ) - } - } - ) { - Checkbox( - checked = !value.contains(c.id), - onCheckedChange = { - if (it) { - onValueChanged( - value.filter { it != c.id } - ) - } else { - onValueChanged( - value + c.id - ) - } - }, - colors = CheckboxDefaults.colors( - checkedColor = Color(c.color) - ) - ) - Text(text = c.name) - } - } - } - TextButton( - onClick = { - onValueChanged(value.toList()) - showDialog = false - }, - modifier = Modifier - .align(Alignment.End) - .padding(vertical = 12.dp, horizontal = 16.dp) - ) { - Text( - text = stringResource(android.R.string.ok), - ) - } - } - } - } - } -} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/calendarwidget/CalendarWidgetSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/calendarwidget/CalendarWidgetSettingsScreenVM.kt deleted file mode 100644 index 9385f21e..00000000 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/calendarwidget/CalendarWidgetSettingsScreenVM.kt +++ /dev/null @@ -1,62 +0,0 @@ -package de.mm20.launcher2.ui.settings.calendarwidget - -import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.ViewModel -import androidx.lifecycle.asLiveData -import androidx.lifecycle.liveData -import androidx.lifecycle.viewModelScope -import de.mm20.launcher2.calendar.CalendarRepository -import de.mm20.launcher2.permissions.PermissionGroup -import de.mm20.launcher2.permissions.PermissionsManager -import de.mm20.launcher2.preferences.LauncherDataStore -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject - -class CalendarWidgetSettingsScreenVM : ViewModel(), KoinComponent { - private val dataStore: LauncherDataStore by inject() - private val calendarRepository: CalendarRepository by inject() - private val permissionsManager: PermissionsManager by inject() - - val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar).asLiveData() - - val excludeAllDayEvents = dataStore.data.map { it.calendarWidget.hideAlldayEvents }.asLiveData() - fun setExcludeAllDayEvents(excludeAllDayEvents: Boolean) { - viewModelScope.launch { - dataStore.updateData { - it.toBuilder() - .setCalendarWidget( - it.calendarWidget - .toBuilder() - .setHideAlldayEvents(excludeAllDayEvents) - ) - .build() - } - } - } - - val calendars = liveData { - emit(calendarRepository.getCalendars()) - } - val unselectedCalendars = - dataStore.data.map { it.calendarWidget.excludeCalendarsList }.asLiveData() - - fun setUnselectedCalendars(unselectedCalendars: List) { - viewModelScope.launch { - dataStore.updateData { - it.toBuilder() - .setCalendarWidget( - it.calendarWidget.toBuilder() - .clearExcludeCalendars() - .addAllExcludeCalendars(unselectedCalendars) - ) - .build() - } - } - } - - fun requestPermission(activity: AppCompatActivity) { - permissionsManager.requestPermission(activity, PermissionGroup.Calendar) - } -} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreen.kt index 84f594f9..e81ff95d 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreen.kt @@ -46,24 +46,17 @@ fun WeatherIntegrationSettingsScreen() { }, value = weatherProvider ) - val imperialUnits by viewModel.imperialUnits.observeAsState(false) - SwitchPreference( - title = stringResource(R.string.preference_imperial_units), - summary = stringResource(R.string.preference_imperial_units_summary), - value = imperialUnits, - onValueChanged = { - viewModel.setImperialUnits(it) - } - ) - val compactMode by viewModel.compactMode.observeAsState(false) - SwitchPreference( - title = stringResource(R.string.preference_compact_mode), - summary = stringResource(R.string.preference_compact_mode_summary), - value = compactMode, - onValueChanged = { - viewModel.setCompactMode(it) - }) } + val imperialUnits by viewModel.imperialUnits.collectAsState(false) + SwitchPreference( + title = stringResource(R.string.preference_imperial_units), + summary = stringResource(R.string.preference_imperial_units_summary), + value = imperialUnits, + onValueChanged = { + viewModel.setImperialUnits(it) + } + ) + } item { PreferenceCategory(title = stringResource(R.string.preference_category_location)) { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreenVM.kt index 9fc6f91f..4455c45f 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreenVM.kt @@ -20,12 +20,17 @@ import org.koin.core.component.inject class WeatherIntegrationSettingsScreenVM : ViewModel(), KoinComponent { private val repository: WeatherRepository by inject() - private val dataStore: LauncherDataStore by inject() private val permissionsManager: PermissionsManager by inject() + private val dataStore: LauncherDataStore by inject() val availableProviders = repository.getAvailableProviders() - val imperialUnits = dataStore.data.map { it.weather.imperialUnits }.asLiveData() + val weatherProvider = repository.selectedProvider.asLiveData() + fun setWeatherProvider(provider: WeatherSettings.WeatherProvider) { + repository.selectProvider(provider) + } + + val imperialUnits = dataStore.data.map { it.weather.imperialUnits } fun setImperialUnits(imperialUnits: Boolean) { viewModelScope.launch { dataStore.updateData { @@ -36,22 +41,6 @@ class WeatherIntegrationSettingsScreenVM : ViewModel(), KoinComponent { } } - val compactMode = dataStore.data.map { it.weather.compactMode }.asLiveData() - fun setCompactMode(compactMode: Boolean) { - viewModelScope.launch { - dataStore.updateData { - it.toBuilder() - .setWeather(it.weather.toBuilder().setCompactMode(compactMode)) - .build() - } - } - } - - val weatherProvider = repository.selectedProvider.asLiveData() - fun setWeatherProvider(provider: WeatherSettings.WeatherProvider) { - repository.selectProvider(provider) - } - val autoLocation = repository.autoLocation.asLiveData() fun setAutoLocation(autoLocation: Boolean) { repository.setAutoLocation(autoLocation) diff --git a/core/i18n/src/main/res/values-cs/strings.xml b/core/i18n/src/main/res/values-cs/strings.xml index 13dac762..35b141ed 100644 --- a/core/i18n/src/main/res/values-cs/strings.xml +++ b/core/i18n/src/main/res/values-cs/strings.xml @@ -574,7 +574,7 @@ Uzamknout rotaci obrazovky na režim na výšku Pevná rotace obrazovky Dynamické barvy - Kompaktní režim + Kompaktní režim Skrýt hodinovou a denní předpověď Spustit při vstupu Spustit zvýrazněnou shodu nebo rychlou akci při klepnutí na Přejít diff --git a/core/i18n/src/main/res/values-de/strings.xml b/core/i18n/src/main/res/values-de/strings.xml index 939a3dfc..359aeca3 100644 --- a/core/i18n/src/main/res/values-de/strings.xml +++ b/core/i18n/src/main/res/values-de/strings.xml @@ -43,7 +43,7 @@ Standort Grad Fahrenheit und Meilen pro Stunde verwenden Imperiale Einheiten - Kompakter Modus + Kompakter Modus Stündliche und tägliche Vorhersagen ausblenden Debug https://de.wikipedia.org diff --git a/core/i18n/src/main/res/values-it/strings.xml b/core/i18n/src/main/res/values-it/strings.xml index 996e56e1..56140f7d 100644 --- a/core/i18n/src/main/res/values-it/strings.xml +++ b/core/i18n/src/main/res/values-it/strings.xml @@ -574,7 +574,7 @@ Rotazione schermo fissa Blocca rotazione dello schermo in modalità verticale Colori dinamici - Modalità compatta + Modalità compatta Nascondi previsioni orarie e giornaliere Nessun icon pack installato Avvia con invio diff --git a/core/i18n/src/main/res/values-nl/strings.xml b/core/i18n/src/main/res/values-nl/strings.xml index 96807e8b..24448211 100644 --- a/core/i18n/src/main/res/values-nl/strings.xml +++ b/core/i18n/src/main/res/values-nl/strings.xml @@ -568,7 +568,7 @@ Vastgezette zoekbalk Vastgezette schermroratie Zet scherm vast in portretmodus - Compacte modus + Compacte modus Uitvoeren bij enter De gemarkeerde app of snelle actie uitvoeren bij tikken op enter Voorspellingen per uur en per dag verbergen diff --git a/core/i18n/src/main/res/values-pt-rBR/strings.xml b/core/i18n/src/main/res/values-pt-rBR/strings.xml index 2366b35e..9a0fe8ee 100644 --- a/core/i18n/src/main/res/values-pt-rBR/strings.xml +++ b/core/i18n/src/main/res/values-pt-rBR/strings.xml @@ -372,7 +372,7 @@ Use graus Fahrenheit e milhas por hora Cartões Personalizar aparência dos cartões - Modo compacto + Modo compacto Ocultar previsões horárias e diárias Cortado Quadrado arredondado diff --git a/core/i18n/src/main/res/values-ru/strings.xml b/core/i18n/src/main/res/values-ru/strings.xml index 838f779a..50dd9c01 100644 --- a/core/i18n/src/main/res/values-ru/strings.xml +++ b/core/i18n/src/main/res/values-ru/strings.xml @@ -507,7 +507,7 @@ Избранные Закрепленные и часто используемые приложения и действия будут находиться здесь Добавить дополнительное пространство над часами, чтобы заполнить всю высоту экрана - Компактный вид + Компактный вид Скрыть почасовой и ежедневный прогноз погоды Заполнить весь экран Цвет diff --git a/core/i18n/src/main/res/values-zh-rCN/strings.xml b/core/i18n/src/main/res/values-zh-rCN/strings.xml index 97e33ff1..b27dda49 100644 --- a/core/i18n/src/main/res/values-zh-rCN/strings.xml +++ b/core/i18n/src/main/res/values-zh-rCN/strings.xml @@ -563,7 +563,7 @@ 在离开主页面视图时禁止滚动搜索栏 修正屏幕方向 锁定屏幕方向为竖屏模式 - 紧凑模式 + 紧凑模式 隐藏每小时和每日预测 动态颜色 没有已安装的图标包 diff --git a/core/i18n/src/main/res/values-zh-rTW/strings.xml b/core/i18n/src/main/res/values-zh-rTW/strings.xml index 0597e4db..8d33dcf0 100644 --- a/core/i18n/src/main/res/values-zh-rTW/strings.xml +++ b/core/i18n/src/main/res/values-zh-rTW/strings.xml @@ -294,7 +294,7 @@ 位置 使用華氏度和英里/小時 英制單位 - 緊湊模式 + 緊湊模式 隱藏每小時和每日預測 除錯 工具 diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index 65d04741..18b15541 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -219,6 +219,7 @@ Calendar Music Favorites + Unknown app widget Add widget More @@ -246,6 +247,7 @@ Adjust height Remove Settings + Replace Edit favorites Not pinned – frequently used @@ -450,7 +452,7 @@ Location Use degrees Fahrenheit and miles per hour Imperial units - Compact mode + Compact mode Hide hourly and daily forecasts Debug Tools @@ -778,4 +780,10 @@ Stable Balanced Variable + Height + Borderless + Configure widget + Weather integration settings + App widget failed to load. + Media control integration settings \ No newline at end of file diff --git a/core/preferences/src/main/proto/settings.proto b/core/preferences/src/main/proto/settings.proto index 2e6c2f10..ca3ef06d 100644 --- a/core/preferences/src/main/proto/settings.proto +++ b/core/preferences/src/main/proto/settings.proto @@ -92,7 +92,7 @@ message Settings { } WeatherProvider provider = 1; bool imperial_units = 2; - bool compact_mode = 3; + reserved 3; } WeatherSettings weather = 5; diff --git a/data/widgets/src/main/java/de/mm20/launcher2/widgets/AppWidget.kt b/data/widgets/src/main/java/de/mm20/launcher2/widgets/AppWidget.kt index 79353483..99db97c1 100644 --- a/data/widgets/src/main/java/de/mm20/launcher2/widgets/AppWidget.kt +++ b/data/widgets/src/main/java/de/mm20/launcher2/widgets/AppWidget.kt @@ -15,16 +15,13 @@ import java.util.UUID data class AppWidgetConfig( val widgetId: Int, val height: Int, + val borderless: Boolean = false, ) data class AppWidget( override val id: UUID, val config: AppWidgetConfig, - val widgetProviderInfo: AppWidgetProviderInfo ) : Widget() { - override fun loadLabel(context: Context): String { - return widgetProviderInfo.loadLabel(context.packageManager) - } override fun toDatabaseEntity(): PartialWidgetEntity { return PartialWidgetEntity( @@ -34,22 +31,6 @@ data class AppWidget( ) } - override val isConfigurable: Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - widgetProviderInfo.widgetFeatures and AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE != 0 - } else { - false - } - - override fun configure(context: Activity, appWidgetHost: AppWidgetHost) { - appWidgetHost.startAppWidgetConfigureActivityForResult( - context, - config.widgetId, - 0, - 0, - null - ) - } - companion object { const val Type = "app" } diff --git a/data/widgets/src/main/java/de/mm20/launcher2/widgets/CalendarWidget.kt b/data/widgets/src/main/java/de/mm20/launcher2/widgets/CalendarWidget.kt index f685277b..d0475993 100644 --- a/data/widgets/src/main/java/de/mm20/launcher2/widgets/CalendarWidget.kt +++ b/data/widgets/src/main/java/de/mm20/launcher2/widgets/CalendarWidget.kt @@ -8,6 +8,8 @@ import android.content.Intent import de.mm20.launcher2.database.entities.PartialWidgetEntity import de.mm20.launcher2.ktx.tryStartActivity import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import java.util.UUID @Serializable @@ -19,33 +21,14 @@ data class CalendarWidget( override val id: UUID, val config: CalendarWidgetConfig = CalendarWidgetConfig(), ) : Widget() { - override fun loadLabel(context: Context): String { - return context.getString(R.string.widget_name_calendar) - } - override fun toDatabaseEntity(): PartialWidgetEntity { return PartialWidgetEntity( id = id, type = Type, - config = null, + config = Json.encodeToString(config), ) } - override val isConfigurable: Boolean = true - - override fun configure(context: Activity, appWidgetHost: AppWidgetHost) { - val intent = Intent() - intent.component = ComponentName( - context.getPackageName(), - "de.mm20.launcher2.ui.settings.SettingsActivity" - ) - intent.putExtra( - "de.mm20.launcher2.settings.ROUTE", - "settings/widgets/calendar" - ) - context.tryStartActivity(intent) - } - companion object { const val Type = "calendar" } diff --git a/data/widgets/src/main/java/de/mm20/launcher2/widgets/FavoritesWidget.kt b/data/widgets/src/main/java/de/mm20/launcher2/widgets/FavoritesWidget.kt new file mode 100644 index 00000000..b3ecfb84 --- /dev/null +++ b/data/widgets/src/main/java/de/mm20/launcher2/widgets/FavoritesWidget.kt @@ -0,0 +1,30 @@ +package de.mm20.launcher2.widgets + +import de.mm20.launcher2.database.entities.PartialWidgetEntity +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.util.UUID + +@Serializable +data class FavoritesWidgetConfig( + val editButton: Boolean = true, +) + +data class FavoritesWidget( + override val id: UUID, + val config: FavoritesWidgetConfig = FavoritesWidgetConfig(), +) : Widget() { + + override fun toDatabaseEntity(): PartialWidgetEntity { + return PartialWidgetEntity( + id = id, + type = Type, + config = Json.encodeToString(config), + ) + } + + companion object { + const val Type = "favorites" + } +} \ No newline at end of file diff --git a/data/widgets/src/main/java/de/mm20/launcher2/widgets/Module.kt b/data/widgets/src/main/java/de/mm20/launcher2/widgets/Module.kt index 2f9d3a46..54375f7d 100644 --- a/data/widgets/src/main/java/de/mm20/launcher2/widgets/Module.kt +++ b/data/widgets/src/main/java/de/mm20/launcher2/widgets/Module.kt @@ -1,8 +1,7 @@ package de.mm20.launcher2.widgets -import org.koin.android.ext.koin.androidContext import org.koin.dsl.module val widgetsModule = module { - single { WidgetRepositoryImpl(androidContext(), get()) } + single { WidgetRepositoryImpl(get()) } } \ No newline at end of file diff --git a/data/widgets/src/main/java/de/mm20/launcher2/widgets/WeatherWidget.kt b/data/widgets/src/main/java/de/mm20/launcher2/widgets/WeatherWidget.kt index 9ee978e8..84f6d857 100644 --- a/data/widgets/src/main/java/de/mm20/launcher2/widgets/WeatherWidget.kt +++ b/data/widgets/src/main/java/de/mm20/launcher2/widgets/WeatherWidget.kt @@ -22,9 +22,6 @@ data class WeatherWidget( override val id: UUID, val config: WeatherWidgetConfig = WeatherWidgetConfig(), ) : Widget() { - override fun loadLabel(context: Context): String { - return context.getString(R.string.widget_name_weather) - } override fun toDatabaseEntity(): PartialWidgetEntity { return PartialWidgetEntity( @@ -34,21 +31,6 @@ data class WeatherWidget( ) } - override val isConfigurable: Boolean = true - - override fun configure(context: Activity, appWidgetHost: AppWidgetHost) { - val intent = Intent() - intent.component = ComponentName( - context.getPackageName(), - "de.mm20.launcher2.ui.settings.SettingsActivity" - ) - intent.putExtra( - "de.mm20.launcher2.settings.ROUTE", - "settings/widgets/weather" - ) - context.tryStartActivity(intent) - } - companion object { const val Type = "weather" } diff --git a/data/widgets/src/main/java/de/mm20/launcher2/widgets/Widget.kt b/data/widgets/src/main/java/de/mm20/launcher2/widgets/Widget.kt index d915c9f9..b8f39d5d 100644 --- a/data/widgets/src/main/java/de/mm20/launcher2/widgets/Widget.kt +++ b/data/widgets/src/main/java/de/mm20/launcher2/widgets/Widget.kt @@ -1,23 +1,15 @@ package de.mm20.launcher2.widgets -import android.app.Activity -import android.appwidget.AppWidgetHost -import android.appwidget.AppWidgetManager -import android.content.ComponentName -import android.content.Context -import android.content.Intent import de.mm20.launcher2.database.entities.PartialWidgetEntity import de.mm20.launcher2.database.entities.WidgetEntity import de.mm20.launcher2.ktx.decodeFromStringOrNull -import de.mm20.launcher2.ktx.tryStartActivity import kotlinx.serialization.json.Json import java.util.UUID sealed class Widget { abstract val id: UUID - abstract fun loadLabel(context: Context): String - fun toDatabaseEntity(position: Int, parentId: UUID? = null): WidgetEntity { + internal fun toDatabaseEntity(position: Int, parentId: UUID? = null): WidgetEntity { return toDatabaseEntity().let { WidgetEntity( id = it.id, @@ -31,11 +23,8 @@ sealed class Widget { abstract fun toDatabaseEntity(): PartialWidgetEntity - open val isConfigurable: Boolean = false - open fun configure(context: Activity, appWidgetHost: AppWidgetHost) {} - companion object { - fun fromDatabaseEntity(context: Context, entity: WidgetEntity): Widget? { + fun fromDatabaseEntity(entity: WidgetEntity): Widget? { return when (entity.type) { WeatherWidget.Type -> { val config: WeatherWidgetConfig = @@ -43,7 +32,6 @@ sealed class Widget { ?: WeatherWidgetConfig() WeatherWidget(entity.id, config) } - MusicWidget.Type -> MusicWidget(entity.id) CalendarWidget.Type -> { val config: CalendarWidgetConfig = @@ -51,8 +39,12 @@ sealed class Widget { ?: CalendarWidgetConfig() CalendarWidget(entity.id, config) } - - FavoritesWidget.Type -> FavoritesWidget(entity.id) + FavoritesWidget.Type -> { + val config: FavoritesWidgetConfig = + Json.decodeFromStringOrNull(entity.config?.takeIf { it.isNotBlank() }) + ?: FavoritesWidgetConfig() + FavoritesWidget(entity.id, config) + } AppWidget.Type -> { val config: AppWidgetConfig = Json.decodeFromStringOrNull(entity.config?.takeIf { it.isNotBlank() }) @@ -60,8 +52,6 @@ sealed class Widget { AppWidget( entity.id, config, - widgetProviderInfo = AppWidgetManager.getInstance(context) - .getAppWidgetInfo(config.widgetId) ) } @@ -75,10 +65,6 @@ sealed class Widget { data class MusicWidget( override val id: UUID, ) : Widget() { - override fun loadLabel(context: Context): String { - return context.getString(R.string.widget_name_music) - } - override fun toDatabaseEntity(): PartialWidgetEntity { return PartialWidgetEntity( id = id, @@ -87,62 +73,12 @@ data class MusicWidget( ) } - override val isConfigurable: Boolean = true - - override fun configure(context: Activity, appWidgetHost: AppWidgetHost) { - val intent = Intent() - intent.component = ComponentName( - context.getPackageName(), - "de.mm20.launcher2.ui.settings.SettingsActivity" - ) - intent.putExtra( - "de.mm20.launcher2.settings.ROUTE", - "settings/widgets/music" - ) - context.tryStartActivity(intent) - } - companion object { const val Type = "music" } } -data class FavoritesWidget( - override val id: UUID, -) : Widget() { - override fun loadLabel(context: Context): String { - return context.getString(R.string.widget_name_favorites) - } - - override fun toDatabaseEntity(): PartialWidgetEntity { - return PartialWidgetEntity( - id = id, - type = Type, - config = null, - ) - } - - override val isConfigurable: Boolean = true - - override fun configure(context: Activity, appWidgetHost: AppWidgetHost) { - val intent = Intent() - intent.component = ComponentName( - context.getPackageName(), - "de.mm20.launcher2.ui.settings.SettingsActivity" - ) - intent.putExtra( - "de.mm20.launcher2.settings.ROUTE", - "settings/favorites" - ) - context.tryStartActivity(intent) - } - - companion object { - const val Type = "favorites" - } -} - enum class WidgetType(val value: String) { INTERNAL("internal"), diff --git a/data/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetRepository.kt b/data/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetRepository.kt index c19721a0..67955409 100644 --- a/data/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetRepository.kt +++ b/data/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetRepository.kt @@ -1,6 +1,5 @@ package de.mm20.launcher2.widgets -import android.content.Context import androidx.room.withTransaction import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.database.AppDatabase @@ -28,7 +27,6 @@ interface WidgetRepository { } internal class WidgetRepositoryImpl( - private val context: Context, private val database: AppDatabase, ) : WidgetRepository { @@ -40,7 +38,7 @@ internal class WidgetRepositoryImpl( } else { dao.queryByParent(parent, limit, offset) }.map { - it.mapNotNull { Widget.fromDatabaseEntity(context, it) } + it.mapNotNull { Widget.fromDatabaseEntity(it) } } }