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.SwipeableState
import androidx.compose.material.swipeable import androidx.compose.material.swipeable
import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.LocalAbsoluteTonalElevation
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocal
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -133,143 +136,147 @@ fun BottomSheetDialog(
} }
} }
Dialog( CompositionLocalProvider(
properties = DialogProperties( LocalAbsoluteTonalElevation provides 0.dp,
dismissOnBackPress = dismissOnBackPress(),
dismissOnClickOutside = swipeToDismiss(),
usePlatformDefaultWidth = false,
),
onDismissRequest = onDismissRequest,
) { ) {
BoxWithConstraints( Dialog(
modifier = Modifier.fillMaxSize(), properties = DialogProperties(
propagateMinConstraints = true, dismissOnBackPress = dismissOnBackPress(),
contentAlignment = Alignment.BottomCenter dismissOnClickOutside = swipeToDismiss(),
usePlatformDefaultWidth = false,
),
onDismissRequest = onDismissRequest,
) { ) {
val maxHeightPx = maxHeight.toPixels() BoxWithConstraints(
val scrimAlpha by animateFloatAsState( modifier = Modifier.fillMaxSize(),
if (swipeState.targetValue == SwipeState.Dismiss) 0f else 0.32f, propagateMinConstraints = true,
label = "Scrim alpha" 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 Box(modifier = Modifier
.background(MaterialTheme.colorScheme.scrim.copy(alpha = scrimAlpha)) .background(MaterialTheme.colorScheme.scrim.copy(alpha = scrimAlpha))
.fillMaxSize() .fillMaxSize()
.pointerInput(onDismissRequest, swipeToDismiss) { .pointerInput(onDismissRequest, swipeToDismiss) {
detectTapGestures { detectTapGestures {
if (swipeToDismiss()) { if (swipeToDismiss()) {
scope.launch { scope.launch {
swipeState.animateTo(SwipeState.Dismiss) swipeState.animateTo(SwipeState.Dismiss)
onDismissRequest() onDismissRequest()
}
} }
} }
} }
} )
)
Column( 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(
modifier = Modifier 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() .fillMaxWidth()
.weight(1f, false), .wrapContentHeight(Alignment.Bottom)
shape = MaterialTheme.shapes.extraLarge.copy( .clipToBounds(),
bottomStart = CornerSize(0), verticalArrangement = Arrangement.Bottom
bottomEnd = CornerSize(0),
),
shadowElevation = 16.dp,
) { ) {
Column { var height by remember {
CenterAlignedTopAppBar( mutableStateOf(maxHeightPx)
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) { LaunchedEffect(null) {
val elevation by animateDpAsState(if (swipeState.offset.value == 0f) 0.dp else 1.dp) swipeState.animateTo(SwipeState.Peek)
val alpha by animateFloatAsState(if (swipeState.targetValue == SwipeState.Dismiss) 0f else 1f) }
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( Surface(
modifier = Modifier modifier = Modifier
.wrapContentHeight() .nestedScroll(nestedScrollConnection)
.fillMaxWidth(), .animateContentSize()
tonalElevation = elevation, .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 modifier = Modifier
.fillMaxWidth() .wrapContentHeight()
.padding(12.dp), .fillMaxWidth(),
horizontalArrangement = Arrangement.End tonalElevation = elevation,
) { ) {
if (dismissButton != null) { Row(
dismissButton() modifier = Modifier
} .fillMaxWidth()
if (confirmButton != null && dismissButton != null) { .padding(12.dp),
Spacer(modifier = Modifier.width(16.dp)) horizontalArrangement = Arrangement.End
} ) {
if (confirmButton != null) { if (dismissButton != null) {
confirmButton() 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( fun SwitchPreference(
title: String, title: String,
icon: ImageVector? = null, icon: ImageVector? = null,
iconPadding: Boolean = true,
summary: String? = null, summary: String? = null,
value: Boolean, value: Boolean,
onValueChanged: (Boolean) -> Unit, onValueChanged: (Boolean) -> Unit,
@ -16,6 +17,7 @@ fun SwitchPreference(
Preference( Preference(
title = title, title = title,
icon = icon, icon = icon,
iconPadding = iconPadding,
summary = summary, summary = summary,
enabled = enabled, enabled = enabled,
onClick = { 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 customizeSearchableSheetShown = mutableStateOf<SavableSearchable?>(null)
val editFavoritesSheetShown = mutableStateOf(false) val editFavoritesSheetShown = mutableStateOf(false)
val hiddenItemsSheetShown = mutableStateOf(false) val hiddenItemsSheetShown = mutableStateOf(false)
val widgetPickerSheetShown = mutableStateOf(false)
init { init {
registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event -> registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
@ -28,7 +27,6 @@ class LauncherBottomSheetManager(registryOwner: SavedStateRegistryOwner) :
editFavoritesSheetShown.value = state?.getBoolean(FAVORITES) ?: false editFavoritesSheetShown.value = state?.getBoolean(FAVORITES) ?: false
hiddenItemsSheetShown.value = state?.getBoolean(HIDDEN) ?: false hiddenItemsSheetShown.value = state?.getBoolean(HIDDEN) ?: false
widgetPickerSheetShown.value = state?.getBoolean(WIDGETS) ?: false
} }
}) })
} }
@ -37,7 +35,6 @@ class LauncherBottomSheetManager(registryOwner: SavedStateRegistryOwner) :
return bundleOf( return bundleOf(
FAVORITES to editFavoritesSheetShown.value, FAVORITES to editFavoritesSheetShown.value,
HIDDEN to hiddenItemsSheetShown.value, HIDDEN to hiddenItemsSheetShown.value,
WIDGETS to widgetPickerSheetShown.value,
) )
} }
@ -65,14 +62,6 @@ class LauncherBottomSheetManager(registryOwner: SavedStateRegistryOwner) :
hiddenItemsSheetShown.value = false hiddenItemsSheetShown.value = false
} }
fun showWidgetPickerSheet() {
widgetPickerSheetShown.value = true
}
fun dismissWidgetPickerSheet() {
widgetPickerSheetShown.value = false
}
companion object { companion object {
private const val PROVIDER = "bottom_sheet_manager" private const val PROVIDER = "bottom_sheet_manager"
private const val FAVORITES = "favorites" private const val FAVORITES = "favorites"

View File

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

View File

@ -199,7 +199,6 @@ private class BindAndConfigureAppWidgetContract(
height = widgetProviderInfo.minHeight, height = widgetProviderInfo.minHeight,
widgetId = widgetId, widgetId = widgetId,
), ),
widgetProviderInfo = widgetProviderInfo,
) )
} }
} }
@ -210,6 +209,7 @@ private class BindAndConfigureAppWidgetContract(
@Composable @Composable
fun WidgetPickerSheet( fun WidgetPickerSheet(
onWidgetSelected: (Widget) -> Unit,
onDismiss: () -> Unit onDismiss: () -> Unit
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -219,7 +219,7 @@ fun WidgetPickerSheet(
val bindAppWidgetStarter = val bindAppWidgetStarter =
rememberLauncherForActivityResult(BindAndConfigureAppWidgetContract()) { rememberLauncherForActivityResult(BindAndConfigureAppWidgetContract()) {
if (it != null) { if (it != null) {
viewModel.pickWidget(it) onWidgetSelected(it)
onDismiss() onDismiss()
} }
} }
@ -290,7 +290,7 @@ fun WidgetPickerSheet(
FavoritesWidget.Type -> FavoritesWidget(id) FavoritesWidget.Type -> FavoritesWidget(id)
else -> return@OutlinedCard else -> return@OutlinedCard
} }
viewModel.pickWidget(widget) onWidgetSelected(widget)
onDismiss() onDismiss()
}) { }) {
Row( Row(

View File

@ -110,11 +110,6 @@ class WidgetPickerSheetVM(
val expandedGroup = mutableStateOf<String?>(null) val expandedGroup = mutableStateOf<String?>(null)
fun pickWidget(widget: Widget) {
val position = enabledWidgets.value.size
widgetsService.addWidget(widget, position)
}
fun toggleGroup(group: String) { fun toggleGroup(group: String) {
expandedGroup.value = if (expandedGroup.value == group) null else group 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.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onPlaced 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.R
import de.mm20.launcher2.ui.ktx.animateTo import de.mm20.launcher2.ui.ktx.animateTo
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
import de.mm20.launcher2.ui.launcher.sheets.WidgetPickerSheet
import de.mm20.launcher2.widgets.AppWidget import de.mm20.launcher2.widgets.AppWidget
import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -56,6 +59,8 @@ fun WidgetColumn(
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = LocalLifecycleOwner.current
val widgetHost = remember { AppWidgetHost(context.applicationContext, 44203) } val widgetHost = remember { AppWidgetHost(context.applicationContext, 44203) }
var addNewWidget by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(null) { LaunchedEffect(null) {
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
widgetHost.startListening() widgetHost.startListening()
@ -166,10 +171,20 @@ fun WidgetColumn(
if (!editMode) { if (!editMode) {
onEditModeChange(true) onEditModeChange(true)
} else { } 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 package de.mm20.launcher2.ui.launcher.widgets
import android.app.Activity
import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetHost
import android.util.Log import android.appwidget.AppWidgetManager
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.gestures.* import androidx.compose.foundation.gestures.DraggableState
import androidx.compose.foundation.layout.* 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.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.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import de.mm20.launcher2.ui.R 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.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.calendar.CalendarWidget
import de.mm20.launcher2.ui.launcher.widgets.external.ExternalWidget 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.favorites.FavoritesWidget
import de.mm20.launcher2.ui.launcher.widgets.music.MusicWidget import de.mm20.launcher2.ui.launcher.widgets.music.MusicWidget
import de.mm20.launcher2.ui.launcher.widgets.weather.WeatherWidget import de.mm20.launcher2.ui.launcher.widgets.weather.WeatherWidget
import de.mm20.launcher2.widgets.* import de.mm20.launcher2.widgets.AppWidget
import kotlin.math.roundToInt 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 @Composable
fun WidgetItem( fun WidgetItem(
@ -44,11 +65,16 @@ fun WidgetItem(
onDragStopped: () -> Unit = {} onDragStopped: () -> Unit = {}
) { ) {
val context = LocalContext.current val context = LocalContext.current
var resizeMode by remember(editMode) { mutableStateOf(false) }
var configure by rememberSaveable { mutableStateOf(false) }
var isDragged by remember { mutableStateOf(false) } var isDragged by remember { mutableStateOf(false) }
val elevation by animateDpAsState(if (isDragged) 8.dp else 2.dp) 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( LauncherCard(
modifier = modifier.zIndex(if (isDragged) 1f else 0f), modifier = modifier.zIndex(if (isDragged) 1f else 0f),
elevation = elevation elevation = elevation
@ -76,7 +102,18 @@ fun WidgetItem(
) )
) )
Text( 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, style = MaterialTheme.typography.titleMedium,
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
@ -84,23 +121,13 @@ fun WidgetItem(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = 1 maxLines = 1
) )
if (widget is AppWidget) { IconButton(onClick = {
IconButton(onClick = { resizeMode = !resizeMode }) { configure = true
Icon( }) {
imageVector = Icons.Rounded.Edit, Icon(
contentDescription = stringResource(R.string.widget_action_adjust_height) imageVector = Icons.Rounded.Tune,
) contentDescription = stringResource(R.string.settings)
} )
}
if (widget.isConfigurable) {
IconButton({
widget.configure(context as Activity, appWidgetHost)
}) {
Icon(
imageVector = Icons.Rounded.Settings,
contentDescription = stringResource(R.string.settings)
)
}
} }
IconButton(onClick = { onWidgetRemove() }) { IconButton(onClick = { onWidgetRemove() }) {
Icon( Icon(
@ -110,61 +137,89 @@ fun WidgetItem(
} }
} }
} }
AnimatedVisibility(!editMode || resizeMode) { AnimatedVisibility(!editMode) {
when (widget) { when (widget) {
is WeatherWidget -> { is WeatherWidget -> {
WeatherWidget() WeatherWidget(widget)
} }
is MusicWidget -> { is MusicWidget -> {
MusicWidget() MusicWidget()
} }
is CalendarWidget -> { is CalendarWidget -> {
CalendarWidget() CalendarWidget(widget)
} }
is FavoritesWidget -> { is FavoritesWidget -> {
FavoritesWidget() FavoritesWidget(widget)
} }
is AppWidget -> { is AppWidget -> {
var dragDelta by remember { mutableStateOf(0) } val widgetInfo = remember(widget.config.widgetId) {
Column { 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( ExternalWidget(
appWidgetHost = appWidgetHost, appWidgetHost = appWidgetHost,
widgetId = widget.config.widgetId, widgetId = widget.config.widgetId,
widgetInfo = widgetInfo,
modifier = Modifier.fillMaxWidth(), 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() 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) { fun removeWidget(widget: Widget) {
widgetRepository.delete(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.InnerCard
import de.mm20.launcher2.ui.component.MissingPermissionBanner import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.launcher.search.common.list.SearchResultList import de.mm20.launcher2.ui.launcher.search.common.list.SearchResultList
import de.mm20.launcher2.widgets.CalendarWidget
import java.time.LocalDate import java.time.LocalDate
import java.time.ZoneId import java.time.ZoneId
@Composable @Composable
fun CalendarWidget() { fun CalendarWidget(
widget: CalendarWidget,
) {
val viewModel: CalendarWidgetVM = viewModel() val viewModel: CalendarWidgetVM = viewModel()
val context = LocalContext.current val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = LocalLifecycleOwner.current
@ -42,6 +45,10 @@ fun CalendarWidget() {
} }
} }
LaunchedEffect(widget) {
viewModel.updateWidget(widget)
}
Column { Column {
Row( Row(
verticalAlignment = Alignment.CenterVertically, 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.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.CalendarEvent import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.services.favorites.FavoritesService 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.collectLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@ -30,11 +33,12 @@ import kotlin.math.max
class CalendarWidgetVM : ViewModel(), KoinComponent { class CalendarWidgetVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val calendarRepository: CalendarRepository by inject() private val calendarRepository: CalendarRepository by inject()
private val favoritesService: FavoritesService by inject() private val favoritesService: FavoritesService by inject()
private val searchableRepository: SearchableRepository by inject() private val searchableRepository: SearchableRepository by inject()
private val widgetConfig = MutableStateFlow(CalendarWidgetConfig())
val calendarEvents = MutableLiveData<List<CalendarEvent>>(emptyList()) val calendarEvents = MutableLiveData<List<CalendarEvent>>(emptyList())
val pinnedCalendarEvents = val pinnedCalendarEvents =
favoritesService.getFavorites( favoritesService.getFavorites(
@ -53,6 +57,10 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
val selectedDate = MutableLiveData(LocalDate.now()) val selectedDate = MutableLiveData(LocalDate.now())
fun updateWidget(widget: CalendarWidget) {
widgetConfig.value = widget.config
}
private var upcomingEvents: List<CalendarEvent> = emptyList() private var upcomingEvents: List<CalendarEvent> = emptyList()
set(value) { set(value) {
field = value field = value
@ -155,10 +163,10 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
suspend fun onActive() { suspend fun onActive() {
selectDate(LocalDate.now()) selectDate(LocalDate.now())
dataStore.data.map { it.calendarWidget }.collectLatest { settings -> widgetConfig.collectLatest { config ->
calendarRepository.getUpcomingEvents( calendarRepository.getUpcomingEvents(
excludeAllDayEvents = settings.hideAlldayEvents, excludeAllDayEvents = !config.allDayEvents,
excludeCalendars = settings.excludeCalendarsList excludeCalendars = config.excludedCalendarIds,
).collectLatest { events -> ).collectLatest { events ->
searchableRepository.getKeys( searchableRepository.getKeys(
includeTypes = listOf(CalendarEvent.Domain), 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.AppWidgetHost
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View import android.view.View
@ -22,20 +23,20 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.doOnNextLayout import androidx.core.view.doOnNextLayout
import androidx.core.view.iterator import androidx.core.view.iterator
import androidx.core.view.setPadding
import de.mm20.launcher2.ui.ktx.toPixels import de.mm20.launcher2.ui.ktx.toPixels
import kotlin.math.roundToInt import kotlin.math.roundToInt
@Composable @Composable
fun ExternalWidget( fun ExternalWidget(
appWidgetHost: AppWidgetHost, appWidgetHost: AppWidgetHost,
widgetInfo: AppWidgetProviderInfo,
widgetId: Int, widgetId: Int,
height: Int, height: Int,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
borderless: Boolean = false,
) { ) {
val context = LocalContext.current val padding = if (borderless) 0 else 8.dp.toPixels().roundToInt()
val widgetInfo = remember(widgetId) {
AppWidgetManager.getInstance(context).getAppWidgetInfo(widgetId)
}
BoxWithConstraints { BoxWithConstraints {
val maxWidth = maxWidth val maxWidth = maxWidth
key(widgetId) { key(widgetId) {
@ -56,6 +57,7 @@ fun ExternalWidget(
maxWidth.value.roundToInt(), maxWidth.value.roundToInt(),
height, 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.common.FavoritesTagSelector
import de.mm20.launcher2.ui.component.Banner import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
import de.mm20.launcher2.widgets.FavoritesWidget
@Composable @Composable
fun FavoritesWidget() { fun FavoritesWidget(widget: FavoritesWidget) {
val viewModel: FavoritesWidgetVM = viewModel() val viewModel: FavoritesWidgetVM = viewModel()
val favorites by remember { viewModel.favorites }.collectAsState(emptyList()) val favorites by remember { viewModel.favorites }.collectAsState(emptyList())
val pinnedTags by viewModel.pinnedTags.collectAsState(emptyList()) val pinnedTags by viewModel.pinnedTags.collectAsState(emptyList())
val selectedTag by viewModel.selectedTag.collectAsState(null) 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) 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.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf 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.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.booleanResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp 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.ui.locals.LocalCardStyle
import de.mm20.launcher2.weather.DailyForecast import de.mm20.launcher2.weather.DailyForecast
import de.mm20.launcher2.weather.Forecast import de.mm20.launcher2.weather.Forecast
import de.mm20.launcher2.widgets.WeatherWidget
import java.text.DateFormat import java.text.DateFormat
import java.text.DecimalFormat import java.text.DecimalFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import kotlin.math.roundToInt import kotlin.math.roundToInt
@Composable @Composable
fun WeatherWidget() { fun WeatherWidget(widget: WeatherWidget) {
val viewModel: WeatherWidgetWM = viewModel() val viewModel: WeatherWidgetWM = viewModel()
val context = LocalContext.current val context = LocalContext.current
@ -82,9 +85,8 @@ fun WeatherWidget() {
val selectedForecast by viewModel.currentForecast.observeAsState() val selectedForecast by viewModel.currentForecast.observeAsState()
val imperialUnits by viewModel.imperialUnits.observeAsState(false) val imperialUnits by viewModel.imperialUnits.collectAsState(false)
val compactMode = !widget.config.showForecast
val compactMode by viewModel.compactMode.observeAsState(false)
var showLocationDialog by remember { mutableStateOf(false) } 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.DailyForecast
import de.mm20.launcher2.weather.Forecast import de.mm20.launcher2.weather.Forecast
import de.mm20.launcher2.weather.WeatherRepository import de.mm20.launcher2.weather.WeatherRepository
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
@ -104,9 +106,7 @@ class WeatherWidgetWM : ViewModel(), KoinComponent {
} }
val autoLocation = weatherRepository.autoLocation.asLiveData() val autoLocation = weatherRepository.autoLocation.asLiveData()
val imperialUnits = dataStore.data.map { it.weather.imperialUnits }.asLiveData() val imperialUnits = dataStore.data.map { it.weather.imperialUnits }
val compactMode = dataStore.data.map { it.weather.compactMode }.asLiveData()
fun selectDay(index: Int) { fun selectDay(index: Int) {
selectedDayIndex = min(index, forecasts.lastIndex) 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.appearance.AppearanceSettingsScreen
import de.mm20.launcher2.ui.settings.backup.BackupSettingsScreen import de.mm20.launcher2.ui.settings.backup.BackupSettingsScreen
import de.mm20.launcher2.ui.settings.buildinfo.BuildInfoSettingsScreen 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.cards.CardsSettingsScreen
import de.mm20.launcher2.ui.settings.clockwidget.ClockWidgetSettingsScreen import de.mm20.launcher2.ui.settings.clockwidget.ClockWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.colorscheme.ColorSchemeSettingsScreen import de.mm20.launcher2.ui.settings.colorscheme.ColorSchemeSettingsScreen
@ -139,15 +138,12 @@ class SettingsActivity : BaseActivity() {
composable("settings/search/tags") { composable("settings/search/tags") {
TagsSettingsScreen() TagsSettingsScreen()
} }
composable("settings/integrations/weather") { composable(ROUTE_WEATHER_INTEGRATION) {
WeatherIntegrationSettingsScreen() WeatherIntegrationSettingsScreen()
} }
composable("settings/integrations/media") { composable(ROUTE_MEDIA_INTEGRATION) {
MediaIntegrationSettingsScreen() MediaIntegrationSettingsScreen()
} }
composable("settings/widgets/calendar") {
CalendarWidgetSettingsScreen()
}
composable("settings/homescreen/clock") { composable("settings/homescreen/clock") {
ClockWidgetSettingsScreen() ClockWidgetSettingsScreen()
} }
@ -214,5 +210,7 @@ class SettingsActivity : BaseActivity() {
companion object { companion object {
const val EXTRA_ROUTE = "de.mm20.launcher2.settings.ROUTE" 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 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 { item {
PreferenceCategory(title = stringResource(R.string.preference_category_location)) { PreferenceCategory(title = stringResource(R.string.preference_category_location)) {

View File

@ -20,12 +20,17 @@ import org.koin.core.component.inject
class WeatherIntegrationSettingsScreenVM : ViewModel(), KoinComponent { class WeatherIntegrationSettingsScreenVM : ViewModel(), KoinComponent {
private val repository: WeatherRepository by inject() private val repository: WeatherRepository by inject()
private val dataStore: LauncherDataStore by inject()
private val permissionsManager: PermissionsManager by inject() private val permissionsManager: PermissionsManager by inject()
private val dataStore: LauncherDataStore by inject()
val availableProviders = repository.getAvailableProviders() 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) { fun setImperialUnits(imperialUnits: Boolean) {
viewModelScope.launch { viewModelScope.launch {
dataStore.updateData { 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() val autoLocation = repository.autoLocation.asLiveData()
fun setAutoLocation(autoLocation: Boolean) { fun setAutoLocation(autoLocation: Boolean) {
repository.setAutoLocation(autoLocation) 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_summary">Uzamknout rotaci obrazovky na režim na výšku</string>
<string name="preference_layout_fixed_rotation">Pevná rotace obrazovky</string> <string name="preference_layout_fixed_rotation">Pevná rotace obrazovky</string>
<string name="icon_pack_dynamic_colors">Dynamické barvy</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_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">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> <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_location">Standort</string>
<string name="preference_imperial_units_summary">Grad Fahrenheit und Meilen pro Stunde verwenden</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_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_compact_mode_summary">Stündliche und tägliche Vorhersagen ausblenden</string>
<string name="preference_category_debug">Debug</string> <string name="preference_category_debug">Debug</string>
<string name="wikipedia_url">https://de.wikipedia.org</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">Rotazione schermo fissa</string>
<string name="preference_layout_fixed_rotation_summary">Blocca rotazione dello schermo in modalità verticale</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="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="preference_compact_mode_summary">Nascondi previsioni orarie e giornaliere</string>
<string name="icon_picker_no_packs_installed">Nessun icon pack installato</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> <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_search_bar">Vastgezette zoekbalk</string>
<string name="preference_layout_fixed_rotation">Vastgezette schermroratie</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_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">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_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> <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_imperial_units_summary">Use graus Fahrenheit e milhas por hora</string>
<string name="preference_cards">Cartões</string> <string name="preference_cards">Cartões</string>
<string name="preference_cards_summary">Personalizar aparência dos 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_compact_mode_summary">Ocultar previsões horárias e diárias</string>
<string name="preference_cards_shape_cut">Cortado</string> <string name="preference_cards_shape_cut">Cortado</string>
<string name="preference_icon_shape_rounded_square">Quadrado arredondado</string> <string name="preference_icon_shape_rounded_square">Quadrado arredondado</string>

View File

@ -507,7 +507,7 @@
<string name="favorites">Избранные</string> <string name="favorites">Избранные</string>
<string name="favorites_empty">Закрепленные и часто используемые приложения и действия будут находиться здесь</string> <string name="favorites_empty">Закрепленные и часто используемые приложения и действия будут находиться здесь</string>
<string name="preference_clock_widget_fill_height_summary">Добавить дополнительное пространство над часами, чтобы заполнить всю высоту экрана</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_compact_mode_summary">Скрыть почасовой и ежедневный прогноз погоды</string>
<string name="preference_clock_widget_fill_height">Заполнить весь экран</string> <string name="preference_clock_widget_fill_height">Заполнить весь экран</string>
<string name="preference_clock_widget_color">Цвет</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_search_bar_summary">在离开主页面视图时禁止滚动搜索栏</string>
<string name="preference_layout_fixed_rotation">修正屏幕方向</string> <string name="preference_layout_fixed_rotation">修正屏幕方向</string>
<string name="preference_layout_fixed_rotation_summary">锁定屏幕方向为竖屏模式</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="preference_compact_mode_summary">隐藏每小时和每日预测</string>
<string name="icon_pack_dynamic_colors">动态颜色</string> <string name="icon_pack_dynamic_colors">动态颜色</string>
<string name="icon_picker_no_packs_installed">没有已安装的图标包</string> <string name="icon_picker_no_packs_installed">没有已安装的图标包</string>

View File

@ -294,7 +294,7 @@
<string name="preference_location">位置</string> <string name="preference_location">位置</string>
<string name="preference_imperial_units_summary">使用華氏度和英里/小時</string> <string name="preference_imperial_units_summary">使用華氏度和英里/小時</string>
<string name="preference_imperial_units">英制單位</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_compact_mode_summary">隱藏每小時和每日預測</string>
<string name="preference_category_debug">除錯</string> <string name="preference_category_debug">除錯</string>
<string name="preference_category_debug_tools">工具</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_calendar">Calendar</string>
<string name="widget_name_music">Music</string> <string name="widget_name_music">Music</string>
<string name="widget_name_favorites">Favorites</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> <string name="widget_add_widget">Add widget</string>
<!-- Add a third party widget (=a standard Android app widget) --> <!-- Add a third party widget (=a standard Android app widget) -->
<string name="widget_add_external">More</string> <string name="widget_add_external">More</string>
@ -246,6 +247,7 @@
<string name="widget_action_adjust_height">Adjust height</string> <string name="widget_action_adjust_height">Adjust height</string>
<string name="widget_action_remove">Remove</string> <string name="widget_action_remove">Remove</string>
<string name="widget_action_settings">Settings</string> <string name="widget_action_settings">Settings</string>
<string name="widget_action_replace">Replace</string>
<string name="menu_item_edit_favs">Edit favorites</string> <string name="menu_item_edit_favs">Edit favorites</string>
<!-- Edit favorites, title for items that are frequently used but not pinned --> <!-- Edit favorites, title for items that are frequently used but not pinned -->
<string name="edit_favorites_dialog_unpinned">Not pinned frequently used</string> <string name="edit_favorites_dialog_unpinned">Not pinned frequently used</string>
@ -450,7 +452,7 @@
<string name="preference_location">Location</string> <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_summary">Use degrees Fahrenheit and miles per hour</string>
<string name="preference_imperial_units">Imperial units</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_compact_mode_summary">Hide hourly and daily forecasts</string>
<string name="preference_category_debug">Debug</string> <string name="preference_category_debug">Debug</string>
<string name="preference_category_debug_tools">Tools</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_low">Stable</string>
<string name="preference_search_result_ordering_weight_factor_default">Balanced</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="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> </resources>

View File

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

View File

@ -15,16 +15,13 @@ import java.util.UUID
data class AppWidgetConfig( data class AppWidgetConfig(
val widgetId: Int, val widgetId: Int,
val height: Int, val height: Int,
val borderless: Boolean = false,
) )
data class AppWidget( data class AppWidget(
override val id: UUID, override val id: UUID,
val config: AppWidgetConfig, val config: AppWidgetConfig,
val widgetProviderInfo: AppWidgetProviderInfo
) : Widget() { ) : Widget() {
override fun loadLabel(context: Context): String {
return widgetProviderInfo.loadLabel(context.packageManager)
}
override fun toDatabaseEntity(): PartialWidgetEntity { override fun toDatabaseEntity(): PartialWidgetEntity {
return 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 { companion object {
const val Type = "app" 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.database.entities.PartialWidgetEntity
import de.mm20.launcher2.ktx.tryStartActivity import de.mm20.launcher2.ktx.tryStartActivity
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.util.UUID import java.util.UUID
@Serializable @Serializable
@ -19,33 +21,14 @@ data class CalendarWidget(
override val id: UUID, override val id: UUID,
val config: CalendarWidgetConfig = CalendarWidgetConfig(), val config: CalendarWidgetConfig = CalendarWidgetConfig(),
) : Widget() { ) : Widget() {
override fun loadLabel(context: Context): String {
return context.getString(R.string.widget_name_calendar)
}
override fun toDatabaseEntity(): PartialWidgetEntity { override fun toDatabaseEntity(): PartialWidgetEntity {
return PartialWidgetEntity( return PartialWidgetEntity(
id = id, id = id,
type = Type, 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 { companion object {
const val Type = "calendar" 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 package de.mm20.launcher2.widgets
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module
val widgetsModule = 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, override val id: UUID,
val config: WeatherWidgetConfig = WeatherWidgetConfig(), val config: WeatherWidgetConfig = WeatherWidgetConfig(),
) : Widget() { ) : Widget() {
override fun loadLabel(context: Context): String {
return context.getString(R.string.widget_name_weather)
}
override fun toDatabaseEntity(): PartialWidgetEntity { override fun toDatabaseEntity(): PartialWidgetEntity {
return 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 { companion object {
const val Type = "weather" const val Type = "weather"
} }

View File

@ -1,23 +1,15 @@
package de.mm20.launcher2.widgets 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.PartialWidgetEntity
import de.mm20.launcher2.database.entities.WidgetEntity import de.mm20.launcher2.database.entities.WidgetEntity
import de.mm20.launcher2.ktx.decodeFromStringOrNull import de.mm20.launcher2.ktx.decodeFromStringOrNull
import de.mm20.launcher2.ktx.tryStartActivity
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.util.UUID import java.util.UUID
sealed class Widget { sealed class Widget {
abstract val id: UUID abstract val id: UUID
abstract fun loadLabel(context: Context): String internal fun toDatabaseEntity(position: Int, parentId: UUID? = null): WidgetEntity {
fun toDatabaseEntity(position: Int, parentId: UUID? = null): WidgetEntity {
return toDatabaseEntity().let { return toDatabaseEntity().let {
WidgetEntity( WidgetEntity(
id = it.id, id = it.id,
@ -31,11 +23,8 @@ sealed class Widget {
abstract fun toDatabaseEntity(): PartialWidgetEntity abstract fun toDatabaseEntity(): PartialWidgetEntity
open val isConfigurable: Boolean = false
open fun configure(context: Activity, appWidgetHost: AppWidgetHost) {}
companion object { companion object {
fun fromDatabaseEntity(context: Context, entity: WidgetEntity): Widget? { fun fromDatabaseEntity(entity: WidgetEntity): Widget? {
return when (entity.type) { return when (entity.type) {
WeatherWidget.Type -> { WeatherWidget.Type -> {
val config: WeatherWidgetConfig = val config: WeatherWidgetConfig =
@ -43,7 +32,6 @@ sealed class Widget {
?: WeatherWidgetConfig() ?: WeatherWidgetConfig()
WeatherWidget(entity.id, config) WeatherWidget(entity.id, config)
} }
MusicWidget.Type -> MusicWidget(entity.id) MusicWidget.Type -> MusicWidget(entity.id)
CalendarWidget.Type -> { CalendarWidget.Type -> {
val config: CalendarWidgetConfig = val config: CalendarWidgetConfig =
@ -51,8 +39,12 @@ sealed class Widget {
?: CalendarWidgetConfig() ?: CalendarWidgetConfig()
CalendarWidget(entity.id, config) CalendarWidget(entity.id, config)
} }
FavoritesWidget.Type -> {
FavoritesWidget.Type -> FavoritesWidget(entity.id) val config: FavoritesWidgetConfig =
Json.decodeFromStringOrNull(entity.config?.takeIf { it.isNotBlank() })
?: FavoritesWidgetConfig()
FavoritesWidget(entity.id, config)
}
AppWidget.Type -> { AppWidget.Type -> {
val config: AppWidgetConfig = val config: AppWidgetConfig =
Json.decodeFromStringOrNull(entity.config?.takeIf { it.isNotBlank() }) Json.decodeFromStringOrNull(entity.config?.takeIf { it.isNotBlank() })
@ -60,8 +52,6 @@ sealed class Widget {
AppWidget( AppWidget(
entity.id, entity.id,
config, config,
widgetProviderInfo = AppWidgetManager.getInstance(context)
.getAppWidgetInfo(config.widgetId)
) )
} }
@ -75,10 +65,6 @@ sealed class Widget {
data class MusicWidget( data class MusicWidget(
override val id: UUID, override val id: UUID,
) : Widget() { ) : Widget() {
override fun loadLabel(context: Context): String {
return context.getString(R.string.widget_name_music)
}
override fun toDatabaseEntity(): PartialWidgetEntity { override fun toDatabaseEntity(): PartialWidgetEntity {
return PartialWidgetEntity( return PartialWidgetEntity(
id = id, 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 { companion object {
const val Type = "music" 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) { enum class WidgetType(val value: String) {
INTERNAL("internal"), INTERNAL("internal"),

View File

@ -1,6 +1,5 @@
package de.mm20.launcher2.widgets package de.mm20.launcher2.widgets
import android.content.Context
import androidx.room.withTransaction import androidx.room.withTransaction
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.AppDatabase
@ -28,7 +27,6 @@ interface WidgetRepository {
} }
internal class WidgetRepositoryImpl( internal class WidgetRepositoryImpl(
private val context: Context,
private val database: AppDatabase, private val database: AppDatabase,
) : WidgetRepository { ) : WidgetRepository {
@ -40,7 +38,7 @@ internal class WidgetRepositoryImpl(
} else { } else {
dao.queryByParent(parent, limit, offset) dao.queryByParent(parent, limit, offset)
}.map { }.map {
it.mapNotNull { Widget.fromDatabaseEntity(context, it) } it.mapNotNull { Widget.fromDatabaseEntity(it) }
} }
} }