Move widget specific settings to widgets

This commit is contained in:
MM20 2023-04-16 14:07:08 +02:00
parent fb87ab20f9
commit 5b2ad94065
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
39 changed files with 958 additions and 645 deletions

View File

@ -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()
}
}
}
}
}
}
}

View File

@ -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,
)
}
)
}

View File

@ -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 = {

View File

@ -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<UserCalendar>()) }
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"
)
}
}

View File

@ -15,7 +15,6 @@ class LauncherBottomSheetManager(registryOwner: SavedStateRegistryOwner) :
val customizeSearchableSheetShown = mutableStateOf<SavableSearchable?>(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"

View File

@ -13,9 +13,4 @@ fun LauncherBottomSheets() {
if (bottomSheetManager.editFavoritesSheetShown.value) {
EditFavoritesSheet(onDismiss = { bottomSheetManager.dismissEditFavoritesSheet() })
}
if (bottomSheetManager.widgetPickerSheetShown.value) {
WidgetPickerSheet(
onDismiss = { bottomSheetManager.dismissWidgetPickerSheet() }
)
}
}

View File

@ -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(

View File

@ -110,11 +110,6 @@ class WidgetPickerSheetVM(
val expandedGroup = mutableStateOf<String?>(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
}

View File

@ -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
}
)
}
}

View File

@ -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
}
)
)
}
}
}
}
}
}
}
}
if (configure) {
ConfigureWidgetSheet(
appWidgetHost = appWidgetHost,
widget = widget,
onWidgetUpdated = onWidgetUpdate,
onDismiss = { configure = false },
)
}
}

View File

@ -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)

View File

@ -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,

View File

@ -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<List<CalendarEvent>>(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<CalendarEvent> = 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),

View File

@ -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)
}
)
}

View File

@ -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)

View File

@ -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) }

View File

@ -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)

View File

@ -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"
}
}

View File

@ -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<UserCalendar>,
value: List<Long>,
onValueChanged: (List<Long>) -> 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),
)
}
}
}
}
}
}

View File

@ -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<Long>) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setCalendarWidget(
it.calendarWidget.toBuilder()
.clearExcludeCalendars()
.addAllExcludeCalendars(unselectedCalendars)
)
.build()
}
}
}
fun requestPermission(activity: AppCompatActivity) {
permissionsManager.requestPermission(activity, PermissionGroup.Calendar)
}
}

View File

@ -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)) {

View File

@ -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)

View File

@ -574,7 +574,7 @@
<string name="preference_layout_fixed_rotation_summary">Uzamknout rotaci obrazovky na režim na výšku</string>
<string name="preference_layout_fixed_rotation">Pevná rotace obrazovky</string>
<string name="icon_pack_dynamic_colors">Dynamické barvy</string>
<string name="preference_compact_mode">Kompaktní režim</string>
<string name="widget_config_weather_compact">Kompaktní režim</string>
<string name="preference_compact_mode_summary">Skrýt hodinovou a denní předpověď</string>
<string name="preference_search_bar_launch_on_enter">Spustit při vstupu</string>
<string name="preference_search_bar_launch_on_enter_summary">Spustit zvýrazněnou shodu nebo rychlou akci při klepnutí na Přejít</string>

View File

@ -43,7 +43,7 @@
<string name="preference_location">Standort</string>
<string name="preference_imperial_units_summary">Grad Fahrenheit und Meilen pro Stunde verwenden</string>
<string name="preference_imperial_units">Imperiale Einheiten</string>
<string name="preference_compact_mode">Kompakter Modus</string>
<string name="widget_config_weather_compact">Kompakter Modus</string>
<string name="preference_compact_mode_summary">Stündliche und tägliche Vorhersagen ausblenden</string>
<string name="preference_category_debug">Debug</string>
<string name="wikipedia_url">https://de.wikipedia.org</string>

View File

@ -574,7 +574,7 @@
<string name="preference_layout_fixed_rotation">Rotazione schermo fissa</string>
<string name="preference_layout_fixed_rotation_summary">Blocca rotazione dello schermo in modalità verticale</string>
<string name="icon_pack_dynamic_colors">Colori dinamici</string>
<string name="preference_compact_mode">Modalità compatta</string>
<string name="widget_config_weather_compact">Modalità compatta</string>
<string name="preference_compact_mode_summary">Nascondi previsioni orarie e giornaliere</string>
<string name="icon_picker_no_packs_installed">Nessun icon pack installato</string>
<string name="preference_search_bar_launch_on_enter">Avvia con invio</string>

View File

@ -568,7 +568,7 @@
<string name="preference_layout_fixed_search_bar">Vastgezette zoekbalk</string>
<string name="preference_layout_fixed_rotation">Vastgezette schermroratie</string>
<string name="preference_layout_fixed_rotation_summary">Zet scherm vast in portretmodus</string>
<string name="preference_compact_mode">Compacte modus</string>
<string name="widget_config_weather_compact">Compacte modus</string>
<string name="preference_search_bar_launch_on_enter">Uitvoeren bij enter</string>
<string name="preference_search_bar_launch_on_enter_summary">De gemarkeerde app of snelle actie uitvoeren bij tikken op enter</string>
<string name="preference_compact_mode_summary">Voorspellingen per uur en per dag verbergen</string>

View File

@ -372,7 +372,7 @@
<string name="preference_imperial_units_summary">Use graus Fahrenheit e milhas por hora</string>
<string name="preference_cards">Cartões</string>
<string name="preference_cards_summary">Personalizar aparência dos cartões</string>
<string name="preference_compact_mode">Modo compacto</string>
<string name="widget_config_weather_compact">Modo compacto</string>
<string name="preference_compact_mode_summary">Ocultar previsões horárias e diárias</string>
<string name="preference_cards_shape_cut">Cortado</string>
<string name="preference_icon_shape_rounded_square">Quadrado arredondado</string>

View File

@ -507,7 +507,7 @@
<string name="favorites">Избранные</string>
<string name="favorites_empty">Закрепленные и часто используемые приложения и действия будут находиться здесь</string>
<string name="preference_clock_widget_fill_height_summary">Добавить дополнительное пространство над часами, чтобы заполнить всю высоту экрана</string>
<string name="preference_compact_mode">Компактный вид</string>
<string name="widget_config_weather_compact">Компактный вид</string>
<string name="preference_compact_mode_summary">Скрыть почасовой и ежедневный прогноз погоды</string>
<string name="preference_clock_widget_fill_height">Заполнить весь экран</string>
<string name="preference_clock_widget_color">Цвет</string>

View File

@ -563,7 +563,7 @@
<string name="preference_layout_fixed_search_bar_summary">在离开主页面视图时禁止滚动搜索栏</string>
<string name="preference_layout_fixed_rotation">修正屏幕方向</string>
<string name="preference_layout_fixed_rotation_summary">锁定屏幕方向为竖屏模式</string>
<string name="preference_compact_mode">紧凑模式</string>
<string name="widget_config_weather_compact">紧凑模式</string>
<string name="preference_compact_mode_summary">隐藏每小时和每日预测</string>
<string name="icon_pack_dynamic_colors">动态颜色</string>
<string name="icon_picker_no_packs_installed">没有已安装的图标包</string>

View File

@ -294,7 +294,7 @@
<string name="preference_location">位置</string>
<string name="preference_imperial_units_summary">使用華氏度和英里/小時</string>
<string name="preference_imperial_units">英制單位</string>
<string name="preference_compact_mode">緊湊模式</string>
<string name="widget_config_weather_compact">緊湊模式</string>
<string name="preference_compact_mode_summary">隱藏每小時和每日預測</string>
<string name="preference_category_debug">除錯</string>
<string name="preference_category_debug_tools">工具</string>

View File

@ -219,6 +219,7 @@
<string name="widget_name_calendar">Calendar</string>
<string name="widget_name_music">Music</string>
<string name="widget_name_favorites">Favorites</string>
<string name="widget_name_unknown">Unknown app widget</string>
<string name="widget_add_widget">Add widget</string>
<!-- Add a third party widget (=a standard Android app widget) -->
<string name="widget_add_external">More</string>
@ -246,6 +247,7 @@
<string name="widget_action_adjust_height">Adjust height</string>
<string name="widget_action_remove">Remove</string>
<string name="widget_action_settings">Settings</string>
<string name="widget_action_replace">Replace</string>
<string name="menu_item_edit_favs">Edit favorites</string>
<!-- Edit favorites, title for items that are frequently used but not pinned -->
<string name="edit_favorites_dialog_unpinned">Not pinned frequently used</string>
@ -450,7 +452,7 @@
<string name="preference_location">Location</string>
<string name="preference_imperial_units_summary">Use degrees Fahrenheit and miles per hour</string>
<string name="preference_imperial_units">Imperial units</string>
<string name="preference_compact_mode">Compact mode</string>
<string name="widget_config_weather_compact">Compact mode</string>
<string name="preference_compact_mode_summary">Hide hourly and daily forecasts</string>
<string name="preference_category_debug">Debug</string>
<string name="preference_category_debug_tools">Tools</string>
@ -778,4 +780,10 @@
<string name="preference_search_result_ordering_weight_factor_low">Stable</string>
<string name="preference_search_result_ordering_weight_factor_default">Balanced</string>
<string name="preference_search_result_ordering_weight_factor_high">Variable</string>
<string name="widget_config_appwidget_height">Height</string>
<string name="widget_config_appwidget_borderless">Borderless</string>
<string name="widget_config_appwidget_configure">Configure widget</string>
<string name="widget_config_weather_integration_settings">Weather integration settings</string>
<string name="app_widget_loading_failed">App widget failed to load.</string>
<string name="widget_config_music_integration_settings">Media control integration settings</string>
</resources>

View File

@ -92,7 +92,7 @@ message Settings {
}
WeatherProvider provider = 1;
bool imperial_units = 2;
bool compact_mode = 3;
reserved 3;
}
WeatherSettings weather = 5;

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}
}

View File

@ -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<WidgetRepository> { WidgetRepositoryImpl(androidContext(), get()) }
single<WidgetRepository> { WidgetRepositoryImpl(get()) }
}

View File

@ -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"
}

View File

@ -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"),

View File

@ -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) }
}
}