Move widget specific settings to widgets
This commit is contained in:
parent
fb87ab20f9
commit
5b2ad94065
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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 = {
|
||||||
|
|||||||
@ -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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
|||||||
@ -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() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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(
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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) }
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)) {
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()) }
|
||||||
}
|
}
|
||||||
@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"),
|
||||||
|
|||||||
@ -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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user