[WIP] Custom widgets as watch face

This commit is contained in:
MM20 2024-04-06 22:44:59 +02:00
parent efc50eb0a8
commit 3288d933c5
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
23 changed files with 682 additions and 373 deletions

View File

@ -0,0 +1,41 @@
package de.mm20.launcher2.ui.base
import android.appwidget.AppWidgetHost
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import de.mm20.launcher2.crashreporter.CrashReporter
import kotlinx.coroutines.awaitCancellation
val LocalAppWidgetHost =
staticCompositionLocalOf<AppWidgetHost> { throw IllegalStateException("AppWidgetHost is not provided") }
@Composable
fun ProvideAppWidgetHost(
content: @Composable () -> Unit,
) {
val lifecycleOwner = LocalLifecycleOwner.current
val context = LocalContext.current
val widgetHost = remember { AppWidgetHost(context.applicationContext, 44203) }
LaunchedEffect(null) {
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
widgetHost.startListening()
try {
awaitCancellation()
} finally {
try {
widgetHost.stopListening()
} catch (e: Exception) {
CrashReporter.logException(e)
}
}
}
}
CompositionLocalProvider(LocalAppWidgetHost provides widgetHost, content = content)
}

View File

@ -0,0 +1,14 @@
package de.mm20.launcher2.ui.base
import androidx.compose.runtime.Composable
@Composable
fun ProvideCompositionLocals(content: @Composable () -> Unit) {
ProvideCurrentTime {
ProvideSettings {
ProvideAppWidgetHost {
content()
}
}
}
}

View File

@ -38,8 +38,7 @@ import de.mm20.launcher2.preferences.BaseLayout
import de.mm20.launcher2.preferences.SystemBarColors
import de.mm20.launcher2.ui.assistant.AssistantScaffold
import de.mm20.launcher2.ui.base.BaseActivity
import de.mm20.launcher2.ui.base.ProvideCurrentTime
import de.mm20.launcher2.ui.base.ProvideSettings
import de.mm20.launcher2.ui.base.ProvideCompositionLocals
import de.mm20.launcher2.ui.component.NavBarEffects
import de.mm20.launcher2.ui.gestures.GestureDetector
import de.mm20.launcher2.ui.gestures.LocalGestureDetector
@ -101,7 +100,7 @@ abstract class SharedLauncherActivity(
LocalGestureDetector provides gestureDetector,
) {
LauncherTheme {
ProvideSettings {
ProvideCompositionLocals {
val statusBarColor by viewModel.statusBarColor.collectAsState()
val navBarColor by viewModel.navBarColor.collectAsState()
@ -160,111 +159,109 @@ abstract class SharedLauncherActivity(
systemUiController.isNavigationBarVisible = !hideNav
}
ProvideCurrentTime {
OverlayHost(
modifier = Modifier
.fillMaxSize()
.background(if (dimBackground) Color.Black.copy(alpha = 0.30f) else Color.Transparent),
contentAlignment = Alignment.BottomCenter
) {
if (chargingAnimation == true) {
NavBarEffects(modifier = Modifier.fillMaxSize())
}
if (mode == LauncherActivityMode.Assistant) {
key(bottomSearchBar, reverseSearchResults) {
AssistantScaffold(
modifier = Modifier
.fillMaxSize(),
darkStatusBarIcons = lightStatus,
darkNavBarIcons = lightNav,
bottomSearchBar = bottomSearchBar,
reverseSearchResults = reverseSearchResults,
fixedSearchBar = fixedSearchBar,
)
}
} else {
when (layout) {
BaseLayout.PullDown -> {
key(bottomSearchBar, reverseSearchResults) {
PullDownScaffold(
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
scaleX =
0.5f + enterTransitionProgress.value * 0.5f
scaleY =
0.5f + enterTransitionProgress.value * 0.5f
alpha = enterTransitionProgress.value
},
darkStatusBarIcons = lightStatus,
darkNavBarIcons = lightNav,
bottomSearchBar = bottomSearchBar,
reverseSearchResults = reverseSearchResults,
fixedSearchBar = fixedSearchBar,
)
}
}
BaseLayout.Pager,
BaseLayout.PagerReversed -> {
key(bottomSearchBar, reverseSearchResults) {
PagerScaffold(
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
scaleX =
0.5f + enterTransitionProgress.value * 0.5f
scaleY =
0.5f + enterTransitionProgress.value * 0.5f
alpha = enterTransitionProgress.value
},
darkStatusBarIcons = lightStatus,
darkNavBarIcons = lightNav,
reverse = layout == BaseLayout.PagerReversed,
bottomSearchBar = bottomSearchBar,
reverseSearchResults = reverseSearchResults,
fixedSearchBar = fixedSearchBar,
)
}
}
else -> {}
}
}
SnackbarHost(
snackbarHostState,
modifier = Modifier
.navigationBarsPadding()
.imePadding()
)
enterTransition?.let {
if (it.startBounds == null || it.targetBounds == null) return@let
val dX = it.startBounds.center.x - it.targetBounds.center.x
val dY = it.startBounds.center.y - it.targetBounds.center.y
val s =
(it.startBounds.minDimension / it.targetBounds.minDimension - 1f) * 0.5f
Box(
modifier = Modifier
.align(Alignment.TopStart)
.graphicsLayer {
val p = (enterTransitionProgress.value).pow(2f)
transformOrigin = TransformOrigin.Center
translationX = it.targetBounds.left + dX * (1 - p)
translationY = it.targetBounds.top + dY * (1 - p)
alpha = enterTransitionProgress.value
scaleX = 1f + s * (1 - p)
scaleY = 1f + s * (1 - p)
}) {
it.icon?.invoke(
Offset(
dX,
dY
)
) { enterTransitionProgress.value }
}
}
LauncherBottomSheets()
OverlayHost(
modifier = Modifier
.fillMaxSize()
.background(if (dimBackground) Color.Black.copy(alpha = 0.30f) else Color.Transparent),
contentAlignment = Alignment.BottomCenter
) {
if (chargingAnimation == true) {
NavBarEffects(modifier = Modifier.fillMaxSize())
}
if (mode == LauncherActivityMode.Assistant) {
key(bottomSearchBar, reverseSearchResults) {
AssistantScaffold(
modifier = Modifier
.fillMaxSize(),
darkStatusBarIcons = lightStatus,
darkNavBarIcons = lightNav,
bottomSearchBar = bottomSearchBar,
reverseSearchResults = reverseSearchResults,
fixedSearchBar = fixedSearchBar,
)
}
} else {
when (layout) {
BaseLayout.PullDown -> {
key(bottomSearchBar, reverseSearchResults) {
PullDownScaffold(
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
scaleX =
0.5f + enterTransitionProgress.value * 0.5f
scaleY =
0.5f + enterTransitionProgress.value * 0.5f
alpha = enterTransitionProgress.value
},
darkStatusBarIcons = lightStatus,
darkNavBarIcons = lightNav,
bottomSearchBar = bottomSearchBar,
reverseSearchResults = reverseSearchResults,
fixedSearchBar = fixedSearchBar,
)
}
}
BaseLayout.Pager,
BaseLayout.PagerReversed -> {
key(bottomSearchBar, reverseSearchResults) {
PagerScaffold(
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
scaleX =
0.5f + enterTransitionProgress.value * 0.5f
scaleY =
0.5f + enterTransitionProgress.value * 0.5f
alpha = enterTransitionProgress.value
},
darkStatusBarIcons = lightStatus,
darkNavBarIcons = lightNav,
reverse = layout == BaseLayout.PagerReversed,
bottomSearchBar = bottomSearchBar,
reverseSearchResults = reverseSearchResults,
fixedSearchBar = fixedSearchBar,
)
}
}
else -> {}
}
}
SnackbarHost(
snackbarHostState,
modifier = Modifier
.navigationBarsPadding()
.imePadding()
)
enterTransition?.let {
if (it.startBounds == null || it.targetBounds == null) return@let
val dX = it.startBounds.center.x - it.targetBounds.center.x
val dY = it.startBounds.center.y - it.targetBounds.center.y
val s =
(it.startBounds.minDimension / it.targetBounds.minDimension - 1f) * 0.5f
Box(
modifier = Modifier
.align(Alignment.TopStart)
.graphicsLayer {
val p = (enterTransitionProgress.value).pow(2f)
transformOrigin = TransformOrigin.Center
translationX = it.targetBounds.left + dX * (1 - p)
translationY = it.targetBounds.top + dY * (1 - p)
alpha = enterTransitionProgress.value
scaleX = 1f + s * (1 - p)
scaleY = 1f + s * (1 - p)
}) {
it.icon?.invoke(
Offset(
dX,
dY
)
) { enterTransitionProgress.value }
}
}
LauncherBottomSheets()
}
}
}

View File

@ -80,6 +80,7 @@ import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.base.LocalAppWidgetHost
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.LargeMessage
import de.mm20.launcher2.ui.component.MissingPermissionBanner
@ -103,7 +104,6 @@ import kotlin.math.roundToInt
@Composable
fun ConfigureWidgetSheet(
appWidgetHost: AppWidgetHost,
widget: Widget,
onWidgetUpdated: (Widget) -> Unit,
onDismiss: () -> Unit,
@ -128,7 +128,7 @@ fun ConfigureWidgetSheet(
) {
when (widget) {
is WeatherWidget -> ConfigureWeatherWidget(widget, onWidgetUpdated)
is AppWidget -> ConfigureAppWidget(appWidgetHost, widget, onWidgetUpdated)
is AppWidget -> ConfigureAppWidget(widget, onWidgetUpdated)
is CalendarWidget -> ConfigureCalendarWidget(widget, onWidgetUpdated)
is FavoritesWidget -> ConfigureFavoritesWidget(widget, onWidgetUpdated)
is MusicWidget -> ConfigureMusicWidget()
@ -272,7 +272,6 @@ fun ColumnScope.ConfigureMusicWidget(
@Composable
fun ColumnScope.ConfigureAppWidget(
appWidgetHost: AppWidgetHost,
widget: AppWidget,
onWidgetUpdated: (Widget) -> Unit,
) {
@ -346,7 +345,6 @@ fun ColumnScope.ConfigureAppWidget(
.background(MaterialTheme.colorScheme.surfaceVariant)
) {
ExternalWidget(
appWidgetHost = appWidgetHost,
widgetInfo = widgetInfo,
widgetId = widget.config.widgetId,
modifier = Modifier.fillMaxWidth(),
@ -472,6 +470,7 @@ fun ColumnScope.ConfigureAppWidget(
}
}
if (isAtLeastApiLevel(28) && widgetInfo.widgetFeatures and AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE != 0) {
val appWidgetHost = LocalAppWidgetHost.current
TextButton(
modifier = Modifier
.padding(top = 8.dp)

View File

@ -251,6 +251,8 @@ private class BindAndConfigureAppWidgetContract(
@Composable
fun WidgetPickerSheet(
includeBuiltinWidgets: Boolean = true,
title: String = stringResource(R.string.widget_pick_widget),
onWidgetSelected: (Widget) -> Unit,
onDismiss: () -> Unit
) {
@ -277,7 +279,7 @@ fun WidgetPickerSheet(
BottomSheetDialog(
onDismissRequest = onDismiss,
title = {
Text(stringResource(R.string.widget_add_widget))
Text(title)
}) {
val builtIn by viewModel.builtInWidgets.collectAsState(emptyList())
LazyColumn(
@ -318,44 +320,46 @@ fun WidgetPickerSheet(
) {
}
}
items(builtIn) {
OutlinedCard(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp, start = 16.dp, end = 16.dp),
onClick = {
val id = UUID.randomUUID()
val widget = when(it.type) {
WeatherWidget.Type -> WeatherWidget(id)
CalendarWidget.Type -> CalendarWidget(id)
MusicWidget.Type -> MusicWidget(id)
FavoritesWidget.Type -> FavoritesWidget(id)
NotesWidget.Type -> NotesWidget(id)
else -> return@OutlinedCard
if (includeBuiltinWidgets) {
items(builtIn) {
OutlinedCard(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp, start = 16.dp, end = 16.dp),
onClick = {
val id = UUID.randomUUID()
val widget = when (it.type) {
WeatherWidget.Type -> WeatherWidget(id)
CalendarWidget.Type -> CalendarWidget(id)
MusicWidget.Type -> MusicWidget(id)
FavoritesWidget.Type -> FavoritesWidget(id)
NotesWidget.Type -> NotesWidget(id)
else -> return@OutlinedCard
}
onWidgetSelected(widget)
onDismiss()
}) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = when (it.type) {
WeatherWidget.Type -> Icons.Rounded.LightMode
CalendarWidget.Type -> Icons.Rounded.Today
MusicWidget.Type -> Icons.Rounded.MusicNote
FavoritesWidget.Type -> Icons.Rounded.Star
NotesWidget.Type -> Icons.Rounded.StickyNote2
else -> Icons.Rounded.Widgets
},
contentDescription = null,
modifier = Modifier.padding(end = 16.dp)
)
Text(
text = it.label,
style = MaterialTheme.typography.titleSmall
)
}
onWidgetSelected(widget)
onDismiss()
}) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = when (it.type) {
WeatherWidget.Type -> Icons.Rounded.LightMode
CalendarWidget.Type -> Icons.Rounded.Today
MusicWidget.Type -> Icons.Rounded.MusicNote
FavoritesWidget.Type -> Icons.Rounded.Star
NotesWidget.Type -> Icons.Rounded.StickyNote2
else -> Icons.Rounded.Widgets
},
contentDescription = null,
modifier = Modifier.padding(end = 16.dp)
)
Text(
text = it.label,
style = MaterialTheme.typography.titleSmall
)
}
}
}

View File

@ -36,6 +36,7 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.base.LocalAppWidgetHost
import de.mm20.launcher2.ui.ktx.animateTo
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
import de.mm20.launcher2.ui.launcher.sheets.WidgetPickerSheet
@ -51,27 +52,11 @@ fun WidgetColumn(
) {
val viewModel: WidgetsVM = viewModel()
val context = LocalContext.current
val bottomSheetManager = LocalBottomSheetManager.current
val lifecycleOwner = LocalLifecycleOwner.current
val widgetHost = remember { AppWidgetHost(context.applicationContext, 44203) }
var addNewWidget by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(null) {
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
widgetHost.startListening()
try {
awaitCancellation()
} finally {
try {
widgetHost.stopListening()
} catch (e: Exception) {
CrashReporter.logException(e)
}
}
}
}
Column(
modifier = modifier
@ -92,9 +77,10 @@ fun WidgetColumn(
dragOffsetAfterSwap = null
}
val widgetHost = LocalAppWidgetHost.current
WidgetItem(
widget = widget,
appWidgetHost = widgetHost,
editMode = editMode,
onWidgetAdd = { widget, offset ->
viewModel.addWidget(widget, i + offset)

View File

@ -60,7 +60,6 @@ import de.mm20.launcher2.widgets.Widget
@Composable
fun WidgetItem(
widget: Widget,
appWidgetHost: AppWidgetHost,
modifier: Modifier = Modifier,
editMode: Boolean = false,
onWidgetAdd: (widget: Widget, offset: Int) -> Unit = { _, _ -> },
@ -222,7 +221,6 @@ fun WidgetItem(
}
} else {
ExternalWidget(
appWidgetHost = appWidgetHost,
widgetId = widget.config.widgetId,
widgetInfo = widgetInfo,
modifier = Modifier.fillMaxWidth(),
@ -237,7 +235,6 @@ fun WidgetItem(
}
if (configure) {
ConfigureWidgetSheet(
appWidgetHost = appWidgetHost,
widget = widget,
onWidgetUpdated = onWidgetUpdate,
onDismiss = { configure = false },

View File

@ -74,6 +74,7 @@ import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.AnalogClock
import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.BinaryClock
import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.CustomClock
import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.DigitalClock1
import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.DigitalClock2
import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.OrbitClock
@ -97,8 +98,11 @@ fun ClockWidget(
val alignment by viewModel.alignment.collectAsState()
val time = LocalTime.current
val darkColors =
color == ClockWidgetColors.Auto && LocalPreferDarkContentOverWallpaper.current || color == ClockWidgetColors.Dark
val contentColor =
if (color == ClockWidgetColors.Auto && LocalPreferDarkContentOverWallpaper.current || color == ClockWidgetColors.Dark) {
if (darkColors) {
Color(0, 0, 0, 180)
} else {
Color.White
@ -182,7 +186,7 @@ fun ClockWidget(
viewModel.launchClockApp(context)
}
) {
Clock(clockStyle, false)
Clock(clockStyle, false, darkColors)
}
if (partProvider != null) {
@ -227,7 +231,7 @@ fun ClockWidget(
viewModel.launchClockApp(context)
}
) {
Clock(clockStyle, true)
Clock(clockStyle, true, darkColors)
}
}
}
@ -248,10 +252,14 @@ fun ClockWidget(
}
}
/**
* @param darkColors: use dark content color / suited for light backgrounds
*/
@Composable
fun Clock(
style: ClockWidgetStyle?,
compact: Boolean,
darkColors: Boolean = false
) {
val time = LocalTime.current
val clockSettings: ClockWidgetSettings by inject()
@ -264,14 +272,51 @@ fun Clock(
style,
compact,
showSeconds,
useThemeColor
useThemeColor,
darkColors
)
is ClockWidgetStyle.Digital2 -> DigitalClock2(time, compact, showSeconds, useThemeColor)
is ClockWidgetStyle.Binary -> BinaryClock(time, compact, showSeconds, useThemeColor)
is ClockWidgetStyle.Analog -> AnalogClock(time, compact, showSeconds, useThemeColor)
is ClockWidgetStyle.Orbit -> OrbitClock(time, compact, showSeconds, useThemeColor)
is ClockWidgetStyle.Segment -> SegmentClock(time, compact, showSeconds, useThemeColor)
is ClockWidgetStyle.Digital2 -> DigitalClock2(
time,
compact,
showSeconds,
useThemeColor,
darkColors
)
is ClockWidgetStyle.Binary -> BinaryClock(
time,
compact,
showSeconds,
useThemeColor,
darkColors
)
is ClockWidgetStyle.Analog -> AnalogClock(
time,
compact,
showSeconds,
useThemeColor,
darkColors
)
is ClockWidgetStyle.Orbit -> OrbitClock(
time,
compact,
showSeconds,
useThemeColor,
darkColors
)
is ClockWidgetStyle.Segment -> SegmentClock(
time,
compact,
showSeconds,
useThemeColor,
darkColors
)
is ClockWidgetStyle.Custom -> CustomClock(style, compact, useThemeColor, darkColors)
is ClockWidgetStyle.Empty -> {}
else -> {}
}
@ -426,7 +471,7 @@ fun ConfigureClockWidgetSheet(
viewModel.setUseThemeColor(it)
}
)
AnimatedVisibility(compact == false) {
AnimatedVisibility(compact == false && style !is ClockWidgetStyle.Custom) {
SwitchPreference(
title = stringResource(R.string.preference_clock_widget_show_seconds),
icon = Icons.Rounded.AccessTime,

View File

@ -1,6 +1,11 @@
package de.mm20.launcher2.ui.launcher.widgets.clock
import android.app.Activity
import android.app.ActivityOptions
import android.appwidget.AppWidgetManager
import android.content.Context
import android.os.Build
import android.util.Log
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
@ -19,8 +24,8 @@ import androidx.compose.material.icons.rounded.Check
import androidx.compose.material.icons.rounded.ChevronLeft
import androidx.compose.material.icons.rounded.ChevronRight
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.icons.rounded.Style
import androidx.compose.material.icons.rounded.Tune
import androidx.compose.material.icons.rounded.Widgets
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
@ -38,6 +43,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -50,8 +56,11 @@ import androidx.compose.ui.zIndex
import de.mm20.launcher2.preferences.ClockWidgetColors
import de.mm20.launcher2.preferences.ClockWidgetStyle
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.base.LocalAppWidgetHost
import de.mm20.launcher2.ui.launcher.sheets.WidgetPickerSheet
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
import de.mm20.launcher2.widgets.AppWidget
import kotlinx.coroutines.launch
@Composable
@ -63,6 +72,9 @@ fun WatchFaceSelector(
onSelect: (ClockWidgetStyle) -> Unit,
) {
val context = LocalContext.current
var showWidgetPicker by rememberSaveable { mutableStateOf(false) }
Surface(
modifier = Modifier
.fillMaxWidth()
@ -78,7 +90,8 @@ fun WatchFaceSelector(
modifier = Modifier,
) {
val pagerState = rememberPagerState(
initialPage = styles.indexOfFirst { it.javaClass == selected?.javaClass }.coerceAtLeast(0),
initialPage = styles.indexOfFirst { it.javaClass == selected?.javaClass }
.coerceAtLeast(0),
) {
styles.size
}
@ -93,7 +106,7 @@ fun WatchFaceSelector(
Box {
androidx.compose.animation.AnimatedVisibility(
selected is ClockWidgetStyle.Digital1,
selected is ClockWidgetStyle.Digital1 || selected is ClockWidgetStyle.Custom,
modifier = Modifier
.align(Alignment.TopEnd)
.zIndex(1f),
@ -123,12 +136,60 @@ fun WatchFaceSelector(
}
)
}
if (selected is ClockWidgetStyle.Custom) {
DropdownMenuItem(
text = { Text(stringResource(R.string.widget_pick_widget)) },
leadingIcon = {
Icon(Icons.Rounded.Widgets, null)
},
onClick = {
showWidgetPicker = true
showStyleSettings = false
}
)
val widget = remember(selected.widgetId) {
val id = selected.widgetId ?: return@remember null
AppWidgetManager.getInstance(context)
.getAppWidgetInfo(id)
}
val appWidgetHost = LocalAppWidgetHost.current
if (widget?.configure != null) {
DropdownMenuItem(
text = { Text(stringResource(R.string.widget_config_appwidget_configure)) },
leadingIcon = {
Icon(Icons.Rounded.Settings, null)
},
onClick = {
appWidgetHost.startAppWidgetConfigureActivityForResult(
context as Activity,
selected.widgetId ?: return@DropdownMenuItem,
0,
0,
if (Build.VERSION.SDK_INT < 34) {
null
} else {
ActivityOptions.makeBasic()
.setPendingIntentBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
)
.setPendingIntentCreatorBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
)
.toBundle()
}
)
}
)
}
}
}
}
}
val darkColors = colors == ClockWidgetColors.Auto && LocalPreferDarkContentOverWallpaper.current || colors == ClockWidgetColors.Dark
CompositionLocalProvider(
LocalContentColor provides if (colors == ClockWidgetColors.Auto && LocalPreferDarkContentOverWallpaper.current || colors == ClockWidgetColors.Dark) {
LocalContentColor provides if (darkColors) {
Color(0, 0, 0, 180)
} else {
Color.White
@ -148,9 +209,9 @@ fun WatchFaceSelector(
) {
val currentPageStyle = styles[pageIndex]
if (currentPageStyle.javaClass == selected?.javaClass) {
Clock(selected, compact)
Clock(selected, compact, darkColors)
} else {
Clock(currentPageStyle, compact)
Clock(currentPageStyle, compact, darkColors)
}
}
}
@ -240,6 +301,23 @@ fun WatchFaceSelector(
}
}
}
if (showWidgetPicker && selected is ClockWidgetStyle.Custom) {
val previousWidgetId = selected.widgetId
val appWidgetHost = LocalAppWidgetHost.current
WidgetPickerSheet(
includeBuiltinWidgets = false,
onWidgetSelected = {
if (previousWidgetId != null) {
appWidgetHost.deleteAppWidgetId(previousWidgetId)
}
onSelect(selected.copy(widgetId = (it as AppWidget).config.widgetId))
},
onDismiss = {
showWidgetPicker = false
}
)
}
}
fun getClockStyleName(context: Context, style: ClockWidgetStyle): String {
@ -251,18 +329,7 @@ fun getClockStyleName(context: Context, style: ClockWidgetStyle): String {
is ClockWidgetStyle.Analog -> context.getString(R.string.clock_style_analog)
is ClockWidgetStyle.Segment -> context.getString(R.string.clock_style_segment)
is ClockWidgetStyle.Empty -> context.getString(R.string.clock_style_empty)
is ClockWidgetStyle.Custom -> context.getString(R.string.clock_style_custom)
else -> ""
}
}
// Compat for old enum names, TODO refactor this screen
object ClockStyle {
val DigitalClock1 = ClockWidgetStyle.Digital1()
val DigitalClock1_Outlined = ClockWidgetStyle.Digital1(outlined = true)
val DigitalClock2 = ClockWidgetStyle.Digital2
val OrbitClock = ClockWidgetStyle.Orbit
val AnalogClock = ClockWidgetStyle.Analog
val BinaryClock = ClockWidgetStyle.Binary
val SegmentClock = ClockWidgetStyle.Segment
val EmptyClock = ClockWidgetStyle.Empty
}

View File

@ -22,6 +22,7 @@ fun AnalogClock(
compact: Boolean,
showSeconds: Boolean,
useThemeColor: Boolean,
darkColors: Boolean,
) {
val verticalLayout = !compact
val date = Calendar.getInstance()
@ -34,7 +35,7 @@ fun AnalogClock(
val strokeWidth = if (verticalLayout) 4.dp else 2.dp
val color = if (useThemeColor) {
if (LocalContentColor.current == Color.White) {
if (!darkColors) {
if (LocalDarkTheme.current) MaterialTheme.colorScheme.onPrimaryContainer
else MaterialTheme.colorScheme.primaryContainer
} else {

View File

@ -23,6 +23,7 @@ fun BinaryClock(
compact: Boolean,
showSeconds: Boolean,
useThemeColor: Boolean,
darkColors: Boolean,
) {
val verticalLayout = !compact
val date = Calendar.getInstance()
@ -33,7 +34,7 @@ fun BinaryClock(
if (hour == 0) hour = 12
val color = if (useThemeColor) {
if (LocalContentColor.current == Color.White) {
if (!darkColors) {
if (LocalDarkTheme.current) MaterialTheme.colorScheme.onPrimaryContainer
else MaterialTheme.colorScheme.primaryContainer
} else {

View File

@ -0,0 +1,49 @@
package de.mm20.launcher2.ui.launcher.widgets.clock.clocks
import android.appwidget.AppWidgetManager
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.Text
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.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.preferences.ClockWidgetStyle
import de.mm20.launcher2.ui.launcher.sheets.WidgetPickerSheet
import de.mm20.launcher2.ui.launcher.widgets.external.ExternalWidget
@Composable
fun CustomClock(
style: ClockWidgetStyle.Custom,
compact: Boolean,
useThemeColor: Boolean,
darkColors: Boolean,
) {
val widgetId = style.widgetId
if (widgetId == null) {
Text("Hmmm…")
} else {
val context = LocalContext.current
val widgetInfo = remember(widgetId) {
AppWidgetManager.getInstance(context)
.getAppWidgetInfo(widgetId)
}
if (widgetInfo != null) {
ExternalWidget(
widgetInfo = widgetInfo,
widgetId = widgetId,
height = if (compact) 64 else 200,
useThemeColors = useThemeColor,
onLightBackground = darkColors,
borderless = compact,
modifier = Modifier.widthIn(max = 250.dp)
)
}
}
}

View File

@ -35,6 +35,7 @@ fun DigitalClock1(
compact: Boolean,
showSeconds: Boolean,
useThemeColor: Boolean,
darkColors: Boolean,
) {
val verticalLayout = !compact
val format = SimpleDateFormat(
@ -56,7 +57,7 @@ fun DigitalClock1(
)
val color = if (useThemeColor) {
if (LocalContentColor.current == Color.White) {
if (!darkColors) {
if (LocalDarkTheme.current) MaterialTheme.colorScheme.onPrimaryContainer
else MaterialTheme.colorScheme.primaryContainer
} else {

View File

@ -22,11 +22,12 @@ fun DigitalClock2(
compact: Boolean,
showSeconds: Boolean,
useThemeColor: Boolean,
darkColors: Boolean,
) {
val verticalLayout = !compact
val color = if (useThemeColor) {
if (LocalContentColor.current == Color.White) {
if (!darkColors) {
if (LocalDarkTheme.current) MaterialTheme.colorScheme.onPrimaryContainer
else MaterialTheme.colorScheme.primaryContainer
} else {

View File

@ -43,15 +43,13 @@ import kotlin.math.sin
private const val PHI_F = 1.618033988749895.toFloat()
private val currentTime
get() = Instant.ofEpochMilli(System.currentTimeMillis()).atZone(ZoneId.systemDefault())
@Composable
fun OrbitClock(
time: Long,
compact: Boolean,
showSeconds: Boolean,
useThemeColor: Boolean
useThemeColor: Boolean,
darkColors: Boolean,
) {
val verticalLayout = !compact
@ -107,8 +105,8 @@ fun OrbitClock(
label = "hoursAnimation"
)
val fgTone = if (LocalContentColor.current == Color.White) 10 else 90
val bgTone = if (LocalContentColor.current == Color.White) 90 else 30
val fgTone = if (!darkColors) 10 else 90
val bgTone = if (!darkColors) 90 else 30
val background = if (useThemeColor) {
Color(TonalPalette.fromInt(MaterialTheme.colorScheme.primaryContainer.toArgb()).tone(bgTone))

View File

@ -50,7 +50,8 @@ fun SegmentClock(
time: Long,
compact: Boolean,
showSeconds: Boolean,
useThemeColor: Boolean
useThemeColor: Boolean,
darkColors: Boolean,
) {
val parsed = Instant.ofEpochMilli(time).atZone(ZoneId.systemDefault())
val hour = parsed.hour
@ -66,7 +67,7 @@ fun SegmentClock(
}
val enabled = if (useThemeColor) {
if (LocalContentColor.current == Color.White) {
if (!darkColors) {
if (LocalDarkTheme.current) MaterialTheme.colorScheme.onPrimaryContainer
else MaterialTheme.colorScheme.primaryContainer
} else {

View File

@ -1,48 +1,53 @@
package de.mm20.launcher2.ui.launcher.widgets.external
import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.os.Bundle
import android.util.Log
import android.os.Build
import android.util.SparseIntArray
import android.view.View
import android.view.ViewGroup
import android.widget.ListView
import android.widget.ScrollView
import androidx.appcompat.app.AppCompatActivity
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.doOnNextLayout
import androidx.core.view.iterator
import androidx.core.view.setPadding
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.ui.base.LocalAppWidgetHost
import de.mm20.launcher2.ui.ktx.toPixels
import palettes.TonalPalette
import kotlin.math.roundToInt
@Composable
fun ExternalWidget(
appWidgetHost: AppWidgetHost,
widgetInfo: AppWidgetProviderInfo,
widgetId: Int,
height: Int,
modifier: Modifier = Modifier,
borderless: Boolean = false,
useThemeColors: Boolean = false,
onLightBackground: Boolean = false,
) {
val padding = if (borderless) 0 else 8.dp.toPixels().roundToInt()
val colorScheme = MaterialTheme.colorScheme
val appWidgetHost = LocalAppWidgetHost.current
BoxWithConstraints {
val maxWidth = maxWidth
key(widgetId) {
AndroidView(
modifier = modifier
.fillMaxWidth()
.height(height.dp),
factory = {
val view = appWidgetHost.createView(it.applicationContext, widgetId, widgetInfo)
@ -50,6 +55,19 @@ fun ExternalWidget(
return@AndroidView view
},
update = {
if (isAtLeastApiLevel(31)) {
if (useThemeColors) {
val colorMapping = getColorMapping(colorScheme)
it.setColorResources(colorMapping)
} else {
it.resetColorResources()
}
}
if (isAtLeastApiLevel(29)) {
it.setOnLightBackground(onLightBackground)
}
it.updateAppWidgetSize(
null,
maxWidth.value.roundToInt(),
@ -71,4 +89,77 @@ private fun enableNestedScroll(view: View) {
}
}
if (view is ListView || view is ScrollView) view.isNestedScrollingEnabled = true
}
@RequiresApi(Build.VERSION_CODES.S)
private fun getColorMapping(colorScheme: ColorScheme): SparseIntArray {
val p = TonalPalette.fromInt(colorScheme.primary.toArgb())
val s = TonalPalette.fromInt(colorScheme.secondary.toArgb())
val t = TonalPalette.fromInt(colorScheme.tertiary.toArgb())
val n = TonalPalette.fromInt(colorScheme.outline.toArgb())
val nv = TonalPalette.fromInt(colorScheme.outlineVariant.toArgb())
val colorResources = SparseIntArray()
colorResources.append(android.R.color.system_accent1_0, p.tone(100))
colorResources.append(android.R.color.system_accent1_10, p.tone(99))
colorResources.append(android.R.color.system_accent1_100, p.tone(90))
colorResources.append(android.R.color.system_accent1_200, p.tone(80))
colorResources.append(android.R.color.system_accent1_300, p.tone(70))
colorResources.append(android.R.color.system_accent1_400, p.tone(60))
colorResources.append(android.R.color.system_accent1_500, p.tone(50))
colorResources.append(android.R.color.system_accent1_600, p.tone(40))
colorResources.append(android.R.color.system_accent1_700, p.tone(30))
colorResources.append(android.R.color.system_accent1_800, p.tone(20))
colorResources.append(android.R.color.system_accent1_900, p.tone(10))
colorResources.append(android.R.color.system_accent1_1000, s.tone(0))
colorResources.append(android.R.color.system_accent2_0, s.tone(100))
colorResources.append(android.R.color.system_accent2_10, s.tone(99))
colorResources.append(android.R.color.system_accent2_100, s.tone(90))
colorResources.append(android.R.color.system_accent2_200, s.tone(80))
colorResources.append(android.R.color.system_accent2_300, s.tone(70))
colorResources.append(android.R.color.system_accent2_400, s.tone(60))
colorResources.append(android.R.color.system_accent2_500, s.tone(50))
colorResources.append(android.R.color.system_accent2_600, s.tone(40))
colorResources.append(android.R.color.system_accent2_700, s.tone(30))
colorResources.append(android.R.color.system_accent2_800, s.tone(20))
colorResources.append(android.R.color.system_accent2_900, s.tone(10))
colorResources.append(android.R.color.system_accent2_1000, t.tone(0))
colorResources.append(android.R.color.system_accent3_0, t.tone(100))
colorResources.append(android.R.color.system_accent3_10, t.tone(99))
colorResources.append(android.R.color.system_accent3_100, t.tone(90))
colorResources.append(android.R.color.system_accent3_200, t.tone(80))
colorResources.append(android.R.color.system_accent3_300, t.tone(70))
colorResources.append(android.R.color.system_accent3_400, t.tone(60))
colorResources.append(android.R.color.system_accent3_500, t.tone(50))
colorResources.append(android.R.color.system_accent3_600, t.tone(40))
colorResources.append(android.R.color.system_accent3_700, t.tone(30))
colorResources.append(android.R.color.system_accent3_800, t.tone(20))
colorResources.append(android.R.color.system_accent3_900, t.tone(10))
colorResources.append(android.R.color.system_accent3_1000, t.tone(0))
colorResources.append(android.R.color.system_neutral1_0, n.tone(100))
colorResources.append(android.R.color.system_neutral1_10, n.tone(99))
colorResources.append(android.R.color.system_neutral1_100, n.tone(90))
colorResources.append(android.R.color.system_neutral1_200, n.tone(80))
colorResources.append(android.R.color.system_neutral1_300, n.tone(70))
colorResources.append(android.R.color.system_neutral1_400, n.tone(60))
colorResources.append(android.R.color.system_neutral1_500, n.tone(50))
colorResources.append(android.R.color.system_neutral1_600, n.tone(40))
colorResources.append(android.R.color.system_neutral1_700, n.tone(30))
colorResources.append(android.R.color.system_neutral1_800, n.tone(20))
colorResources.append(android.R.color.system_neutral1_900, n.tone(10))
colorResources.append(android.R.color.system_neutral1_1000, nv.tone(0))
colorResources.append(android.R.color.system_neutral2_0, nv.tone(100))
colorResources.append(android.R.color.system_neutral2_10, nv.tone(99))
colorResources.append(android.R.color.system_neutral2_100, nv.tone(90))
colorResources.append(android.R.color.system_neutral2_200, nv.tone(80))
colorResources.append(android.R.color.system_neutral2_300, nv.tone(70))
colorResources.append(android.R.color.system_neutral2_400, nv.tone(60))
colorResources.append(android.R.color.system_neutral2_500, nv.tone(50))
colorResources.append(android.R.color.system_neutral2_600, nv.tone(40))
colorResources.append(android.R.color.system_neutral2_700, nv.tone(30))
colorResources.append(android.R.color.system_neutral2_800, nv.tone(20))
colorResources.append(android.R.color.system_neutral2_900, nv.tone(10))
colorResources.append(android.R.color.system_neutral2_1000, nv.tone(0))
return colorResources
}

View File

@ -11,8 +11,6 @@ import de.mm20.launcher2.ui.theme.WallpaperColors
val LocalWindowSize = compositionLocalOf { Size(0f, 0f) }
val LocalAppWidgetHost = compositionLocalOf<AppWidgetHost?>(defaultFactory = { null })
val LocalNavController = compositionLocalOf<NavController?> { null }
val LocalCardStyle = compositionLocalOf { CardStyle() }

View File

@ -24,8 +24,7 @@ import androidx.navigation.navArgument
import de.mm20.launcher2.licenses.AppLicense
import de.mm20.launcher2.licenses.OpenSourceLicenses
import de.mm20.launcher2.ui.base.BaseActivity
import de.mm20.launcher2.ui.base.ProvideCurrentTime
import de.mm20.launcher2.ui.base.ProvideSettings
import de.mm20.launcher2.ui.base.ProvideCompositionLocals
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalNavController
import de.mm20.launcher2.ui.locals.LocalWallpaperColors
@ -84,158 +83,161 @@ class SettingsActivity : BaseActivity() {
LocalNavController provides navController,
LocalWallpaperColors provides wallpaperColors,
) {
ProvideSettings {
ProvideCurrentTime {
LauncherTheme {
val systemBarColor = MaterialTheme.colorScheme.surfaceDim
val systemBarColorAlt = MaterialTheme.colorScheme.onSurface
val isDarkTheme = LocalDarkTheme.current
LaunchedEffect(isDarkTheme, systemBarColor, systemBarColorAlt) {
enableEdgeToEdge(
if (isDarkTheme) SystemBarStyle.dark(systemBarColor.toArgb())
else SystemBarStyle.light(systemBarColor.toArgb(), systemBarColorAlt.toArgb())
ProvideCompositionLocals {
LauncherTheme {
val systemBarColor = MaterialTheme.colorScheme.surfaceDim
val systemBarColorAlt = MaterialTheme.colorScheme.onSurface
val isDarkTheme = LocalDarkTheme.current
LaunchedEffect(isDarkTheme, systemBarColor, systemBarColorAlt) {
enableEdgeToEdge(
if (isDarkTheme) SystemBarStyle.dark(systemBarColor.toArgb())
else SystemBarStyle.light(
systemBarColor.toArgb(),
systemBarColorAlt.toArgb()
)
}
OverlayHost {
NavHost(
navController = navController,
startDestination = "settings",
exitTransition = {
fadeOut() + scaleOut(targetScale = 0.5f)
},
enterTransition = {
slideInHorizontally { it }
},
popEnterTransition = {
fadeIn() + scaleIn(initialScale = 0.5f)
},
popExitTransition = {
slideOutHorizontally { it }
},
)
}
OverlayHost {
NavHost(
navController = navController,
startDestination = "settings",
exitTransition = {
fadeOut() + scaleOut(targetScale = 0.5f)
},
enterTransition = {
slideInHorizontally { it }
},
popEnterTransition = {
fadeIn() + scaleIn(initialScale = 0.5f)
},
popExitTransition = {
slideOutHorizontally { it }
},
) {
composable("settings") {
MainSettingsScreen()
}
composable("settings/appearance") {
AppearanceSettingsScreen()
}
composable("settings/homescreen") {
HomescreenSettingsScreen()
}
composable("settings/icons") {
IconsSettingsScreen()
}
composable("settings/appearance/themes") {
ThemesSettingsScreen()
}
composable(
"settings/appearance/themes/{id}",
arguments = listOf(navArgument("id") {
nullable = false
})
) {
composable("settings") {
MainSettingsScreen()
}
composable("settings/appearance") {
AppearanceSettingsScreen()
}
composable("settings/homescreen") {
HomescreenSettingsScreen()
}
composable("settings/icons") {
IconsSettingsScreen()
}
composable("settings/appearance/themes") {
ThemesSettingsScreen()
}
composable(
"settings/appearance/themes/{id}",
arguments = listOf(navArgument("id") {
nullable = false
})
) {
val id = it.arguments?.getString("id")?.let {
UUID.fromString(it)
} ?: return@composable
ThemeSettingsScreen(id)
}
composable("settings/appearance/cards") {
CardsSettingsScreen()
}
composable("settings/search") {
SearchSettingsScreen()
}
composable("settings/gestures") {
GestureSettingsScreen()
}
composable("settings/search/unitconverter") {
UnitConverterSettingsScreen()
}
composable("settings/search/wikipedia") {
WikipediaSettingsScreen()
}
composable("settings/search/locations") {
LocationsSettingsScreen()
}
composable("settings/search/files") {
FileSearchSettingsScreen()
}
composable("settings/search/searchactions") {
SearchActionsSettingsScreen()
}
composable("settings/search/hiddenitems") {
HiddenItemsSettingsScreen()
}
composable("settings/search/tags") {
TagsSettingsScreen()
}
composable(ROUTE_WEATHER_INTEGRATION) {
WeatherIntegrationSettingsScreen()
}
composable(ROUTE_MEDIA_INTEGRATION) {
MediaIntegrationSettingsScreen()
}
composable("settings/favorites") {
FavoritesSettingsScreen()
}
composable("settings/integrations") {
IntegrationsSettingsScreen()
}
composable("settings/plugins") {
PluginsSettingsScreen()
}
composable("settings/plugins/{id}") {
PluginSettingsScreen(it.arguments?.getString("id") ?: return@composable)
}
composable("settings/about") {
AboutSettingsScreen()
}
composable("settings/about/buildinfo") {
BuildInfoSettingsScreen()
}
composable("settings/about/easteregg") {
EasterEggSettingsScreen()
}
composable("settings/debug") {
DebugSettingsScreen()
}
composable("settings/backup") {
BackupSettingsScreen()
}
composable("settings/debug/crashreporter") {
CrashReporterScreen()
}
composable("settings/debug/logs") {
LogScreen()
}
composable(
"settings/debug/crashreporter/report?fileName={fileName}",
arguments = listOf(navArgument("fileName") {
nullable = false
})
) {
val fileName = it.arguments?.getString("fileName")
?.let {
URLDecoder.decode(it, "utf8")
}
CrashReportScreen(fileName!!)
}
composable(
"settings/license?library={libraryName}",
arguments = listOf(navArgument("libraryName") {
nullable = true
})
) {
val libraryName = it.arguments?.getString("libraryName")
val library = remember(libraryName) {
if (libraryName == null) {
AppLicense.get(this@SettingsActivity)
} else {
OpenSourceLicenses.first { it.name == libraryName }
}
val id = it.arguments?.getString("id")?.let {
UUID.fromString(it)
} ?: return@composable
ThemeSettingsScreen(id)
}
composable("settings/appearance/cards") {
CardsSettingsScreen()
}
composable("settings/search") {
SearchSettingsScreen()
}
composable("settings/gestures") {
GestureSettingsScreen()
}
composable("settings/search/unitconverter") {
UnitConverterSettingsScreen()
}
composable("settings/search/wikipedia") {
WikipediaSettingsScreen()
}
composable("settings/search/locations") {
LocationsSettingsScreen()
}
composable("settings/search/files") {
FileSearchSettingsScreen()
}
composable("settings/search/searchactions") {
SearchActionsSettingsScreen()
}
composable("settings/search/hiddenitems") {
HiddenItemsSettingsScreen()
}
composable("settings/search/tags") {
TagsSettingsScreen()
}
composable(ROUTE_WEATHER_INTEGRATION) {
WeatherIntegrationSettingsScreen()
}
composable(ROUTE_MEDIA_INTEGRATION) {
MediaIntegrationSettingsScreen()
}
composable("settings/favorites") {
FavoritesSettingsScreen()
}
composable("settings/integrations") {
IntegrationsSettingsScreen()
}
composable("settings/plugins") {
PluginsSettingsScreen()
}
composable("settings/plugins/{id}") {
PluginSettingsScreen(
it.arguments?.getString("id") ?: return@composable
)
}
composable("settings/about") {
AboutSettingsScreen()
}
composable("settings/about/buildinfo") {
BuildInfoSettingsScreen()
}
composable("settings/about/easteregg") {
EasterEggSettingsScreen()
}
composable("settings/debug") {
DebugSettingsScreen()
}
composable("settings/backup") {
BackupSettingsScreen()
}
composable("settings/debug/crashreporter") {
CrashReporterScreen()
}
composable("settings/debug/logs") {
LogScreen()
}
composable(
"settings/debug/crashreporter/report?fileName={fileName}",
arguments = listOf(navArgument("fileName") {
nullable = false
})
) {
val fileName = it.arguments?.getString("fileName")
?.let {
URLDecoder.decode(it, "utf8")
}
CrashReportScreen(fileName!!)
}
composable(
"settings/license?library={libraryName}",
arguments = listOf(navArgument("libraryName") {
nullable = true
})
) {
val libraryName = it.arguments?.getString("libraryName")
val library = remember(libraryName) {
if (libraryName == null) {
AppLicense.get(this@SettingsActivity)
} else {
OpenSourceLicenses.first { it.name == libraryName }
}
LicenseScreen(library)
}
LicenseScreen(library)
}
}
}

View File

@ -7,6 +7,7 @@ import de.mm20.launcher2.preferences.ClockWidgetColors
import de.mm20.launcher2.preferences.ClockWidgetStyle
import de.mm20.launcher2.preferences.ui.ClockWidgetSettings
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import org.koin.core.component.KoinComponent
@ -20,7 +21,7 @@ class ClockWidgetSettingsScreenVM : ViewModel(), KoinComponent {
settings.setCompact(compact)
}
val availableClockStyles = settings.digital1.map {digital1 ->
val availableClockStyles = combine(settings.digital1, settings.custom) {digital1, custom ->
listOf(
digital1,
ClockWidgetStyle.Digital2,
@ -28,6 +29,7 @@ class ClockWidgetSettingsScreenVM : ViewModel(), KoinComponent {
ClockWidgetStyle.Orbit,
ClockWidgetStyle.Segment,
ClockWidgetStyle.Binary,
custom,
ClockWidgetStyle.Empty,
)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())

View File

@ -845,6 +845,7 @@
<string name="widget_config_appwidget_resize_hint">Drag to resize</string>
<string name="widget_config_weather_integration_settings">Weather integration settings</string>
<string name="widget_config_calendar_no_calendars">No calendars found</string>
<string name="widget_pick_widget">Pick widget</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>
<string name="note_widget_link_file">Link to file</string>
@ -909,6 +910,7 @@
<string name="clock_style_analog">Hands</string>
<string name="clock_style_segment">7-segment</string>
<string name="clock_style_empty">No clock</string>
<string name="clock_style_custom">Custom widget</string>
<string name="clock_variant_standard">Standard</string>
<string name="clock_variant_outlined">Outlined</string>
</resources>

View File

@ -30,6 +30,7 @@ data class LauncherSettingsData internal constructor(
@SerialName("clockWidgetStyle2")
internal val clockWidgetStyle: ClockWidgetStyleEnum = ClockWidgetStyleEnum.Digital1,
val clockWidgetDigital1: ClockWidgetStyle.Digital1 = ClockWidgetStyle.Digital1(),
val clockWidgetCustom: ClockWidgetStyle.Custom = ClockWidgetStyle.Custom(),
val clockWidgetColors: ClockWidgetColors = ClockWidgetColors.Auto,
val clockWidgetShowSeconds: Boolean = false,
val clockWidgetUseThemeColor: Boolean = false,
@ -192,6 +193,7 @@ internal enum class ClockWidgetStyleEnum {
Binary,
Segment,
Empty,
Custom,
}
@Serializable
@ -234,6 +236,10 @@ sealed interface ClockWidgetStyle {
@Serializable
@SerialName("empty")
data object Empty : ClockWidgetStyle
@Serializable
@SerialName("custom")
data class Custom(val widgetId: Int? = null) : ClockWidgetStyle
}
@Serializable

View File

@ -99,17 +99,22 @@ class ClockWidgetSettings internal constructor(
ClockWidgetStyleEnum.Binary -> ClockWidgetStyle.Binary
ClockWidgetStyleEnum.Segment -> ClockWidgetStyle.Segment
ClockWidgetStyleEnum.Empty -> ClockWidgetStyle.Empty
ClockWidgetStyleEnum.Custom -> it.clockWidgetCustom
}
}
val digital1: Flow<ClockWidgetStyle.Digital1>
get() = launcherDataStore.data.map { it.clockWidgetDigital1 }
val custom: Flow<ClockWidgetStyle.Custom>
get() = launcherDataStore.data.map { it.clockWidgetCustom }
fun setClockStyle(clockStyle: ClockWidgetStyle) {
launcherDataStore.update {
it.copy(
clockWidgetStyle = clockStyle.enumValue,
clockWidgetDigital1 = if (clockStyle is ClockWidgetStyle.Digital1) clockStyle else it.clockWidgetDigital1,
clockWidgetCustom = if (clockStyle is ClockWidgetStyle.Custom) clockStyle else it.clockWidgetCustom,
)
}
}
@ -151,4 +156,5 @@ internal val ClockWidgetStyle.enumValue
is ClockWidgetStyle.Binary -> ClockWidgetStyleEnum.Binary
is ClockWidgetStyle.Segment -> ClockWidgetStyleEnum.Segment
is ClockWidgetStyle.Empty -> ClockWidgetStyleEnum.Empty
is ClockWidgetStyle.Custom -> ClockWidgetStyleEnum.Custom
}