[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.preferences.SystemBarColors
import de.mm20.launcher2.ui.assistant.AssistantScaffold import de.mm20.launcher2.ui.assistant.AssistantScaffold
import de.mm20.launcher2.ui.base.BaseActivity import de.mm20.launcher2.ui.base.BaseActivity
import de.mm20.launcher2.ui.base.ProvideCurrentTime import de.mm20.launcher2.ui.base.ProvideCompositionLocals
import de.mm20.launcher2.ui.base.ProvideSettings
import de.mm20.launcher2.ui.component.NavBarEffects import de.mm20.launcher2.ui.component.NavBarEffects
import de.mm20.launcher2.ui.gestures.GestureDetector import de.mm20.launcher2.ui.gestures.GestureDetector
import de.mm20.launcher2.ui.gestures.LocalGestureDetector import de.mm20.launcher2.ui.gestures.LocalGestureDetector
@ -101,7 +100,7 @@ abstract class SharedLauncherActivity(
LocalGestureDetector provides gestureDetector, LocalGestureDetector provides gestureDetector,
) { ) {
LauncherTheme { LauncherTheme {
ProvideSettings { ProvideCompositionLocals {
val statusBarColor by viewModel.statusBarColor.collectAsState() val statusBarColor by viewModel.statusBarColor.collectAsState()
val navBarColor by viewModel.navBarColor.collectAsState() val navBarColor by viewModel.navBarColor.collectAsState()
@ -160,111 +159,109 @@ abstract class SharedLauncherActivity(
systemUiController.isNavigationBarVisible = !hideNav systemUiController.isNavigationBarVisible = !hideNav
} }
ProvideCurrentTime { OverlayHost(
OverlayHost( modifier = Modifier
modifier = Modifier .fillMaxSize()
.fillMaxSize() .background(if (dimBackground) Color.Black.copy(alpha = 0.30f) else Color.Transparent),
.background(if (dimBackground) Color.Black.copy(alpha = 0.30f) else Color.Transparent), contentAlignment = Alignment.BottomCenter
contentAlignment = Alignment.BottomCenter ) {
) { if (chargingAnimation == true) {
if (chargingAnimation == true) { NavBarEffects(modifier = Modifier.fillMaxSize())
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()
} }
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.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.ui.R 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.BottomSheetDialog
import de.mm20.launcher2.ui.component.LargeMessage import de.mm20.launcher2.ui.component.LargeMessage
import de.mm20.launcher2.ui.component.MissingPermissionBanner import de.mm20.launcher2.ui.component.MissingPermissionBanner
@ -103,7 +104,6 @@ import kotlin.math.roundToInt
@Composable @Composable
fun ConfigureWidgetSheet( fun ConfigureWidgetSheet(
appWidgetHost: AppWidgetHost,
widget: Widget, widget: Widget,
onWidgetUpdated: (Widget) -> Unit, onWidgetUpdated: (Widget) -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
@ -128,7 +128,7 @@ fun ConfigureWidgetSheet(
) { ) {
when (widget) { when (widget) {
is WeatherWidget -> ConfigureWeatherWidget(widget, onWidgetUpdated) is WeatherWidget -> ConfigureWeatherWidget(widget, onWidgetUpdated)
is AppWidget -> ConfigureAppWidget(appWidgetHost, widget, onWidgetUpdated) is AppWidget -> ConfigureAppWidget(widget, onWidgetUpdated)
is CalendarWidget -> ConfigureCalendarWidget(widget, onWidgetUpdated) is CalendarWidget -> ConfigureCalendarWidget(widget, onWidgetUpdated)
is FavoritesWidget -> ConfigureFavoritesWidget(widget, onWidgetUpdated) is FavoritesWidget -> ConfigureFavoritesWidget(widget, onWidgetUpdated)
is MusicWidget -> ConfigureMusicWidget() is MusicWidget -> ConfigureMusicWidget()
@ -272,7 +272,6 @@ fun ColumnScope.ConfigureMusicWidget(
@Composable @Composable
fun ColumnScope.ConfigureAppWidget( fun ColumnScope.ConfigureAppWidget(
appWidgetHost: AppWidgetHost,
widget: AppWidget, widget: AppWidget,
onWidgetUpdated: (Widget) -> Unit, onWidgetUpdated: (Widget) -> Unit,
) { ) {
@ -346,7 +345,6 @@ fun ColumnScope.ConfigureAppWidget(
.background(MaterialTheme.colorScheme.surfaceVariant) .background(MaterialTheme.colorScheme.surfaceVariant)
) { ) {
ExternalWidget( ExternalWidget(
appWidgetHost = appWidgetHost,
widgetInfo = widgetInfo, widgetInfo = widgetInfo,
widgetId = widget.config.widgetId, widgetId = widget.config.widgetId,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -472,6 +470,7 @@ fun ColumnScope.ConfigureAppWidget(
} }
} }
if (isAtLeastApiLevel(28) && widgetInfo.widgetFeatures and AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE != 0) { if (isAtLeastApiLevel(28) && widgetInfo.widgetFeatures and AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE != 0) {
val appWidgetHost = LocalAppWidgetHost.current
TextButton( TextButton(
modifier = Modifier modifier = Modifier
.padding(top = 8.dp) .padding(top = 8.dp)

View File

@ -251,6 +251,8 @@ private class BindAndConfigureAppWidgetContract(
@Composable @Composable
fun WidgetPickerSheet( fun WidgetPickerSheet(
includeBuiltinWidgets: Boolean = true,
title: String = stringResource(R.string.widget_pick_widget),
onWidgetSelected: (Widget) -> Unit, onWidgetSelected: (Widget) -> Unit,
onDismiss: () -> Unit onDismiss: () -> Unit
) { ) {
@ -277,7 +279,7 @@ fun WidgetPickerSheet(
BottomSheetDialog( BottomSheetDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { title = {
Text(stringResource(R.string.widget_add_widget)) Text(title)
}) { }) {
val builtIn by viewModel.builtInWidgets.collectAsState(emptyList()) val builtIn by viewModel.builtInWidgets.collectAsState(emptyList())
LazyColumn( LazyColumn(
@ -318,44 +320,46 @@ fun WidgetPickerSheet(
) { ) {
} }
} }
items(builtIn) { if (includeBuiltinWidgets) {
OutlinedCard( items(builtIn) {
modifier = Modifier OutlinedCard(
.fillMaxWidth() modifier = Modifier
.padding(bottom = 16.dp, start = 16.dp, end = 16.dp), .fillMaxWidth()
onClick = { .padding(bottom = 16.dp, start = 16.dp, end = 16.dp),
val id = UUID.randomUUID() onClick = {
val widget = when(it.type) { val id = UUID.randomUUID()
WeatherWidget.Type -> WeatherWidget(id) val widget = when (it.type) {
CalendarWidget.Type -> CalendarWidget(id) WeatherWidget.Type -> WeatherWidget(id)
MusicWidget.Type -> MusicWidget(id) CalendarWidget.Type -> CalendarWidget(id)
FavoritesWidget.Type -> FavoritesWidget(id) MusicWidget.Type -> MusicWidget(id)
NotesWidget.Type -> NotesWidget(id) FavoritesWidget.Type -> FavoritesWidget(id)
else -> return@OutlinedCard 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 androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.ui.R 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.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.ui.launcher.sheets.WidgetPickerSheet
@ -51,27 +52,11 @@ fun WidgetColumn(
) { ) {
val viewModel: WidgetsVM = viewModel() val viewModel: WidgetsVM = viewModel()
val context = LocalContext.current
val bottomSheetManager = LocalBottomSheetManager.current val bottomSheetManager = LocalBottomSheetManager.current
val lifecycleOwner = LocalLifecycleOwner.current
val widgetHost = remember { AppWidgetHost(context.applicationContext, 44203) }
var addNewWidget by rememberSaveable { mutableStateOf(false) } 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( Column(
modifier = modifier modifier = modifier
@ -92,9 +77,10 @@ fun WidgetColumn(
dragOffsetAfterSwap = null dragOffsetAfterSwap = null
} }
val widgetHost = LocalAppWidgetHost.current
WidgetItem( WidgetItem(
widget = widget, widget = widget,
appWidgetHost = widgetHost,
editMode = editMode, editMode = editMode,
onWidgetAdd = { widget, offset -> onWidgetAdd = { widget, offset ->
viewModel.addWidget(widget, i + offset) viewModel.addWidget(widget, i + offset)

View File

@ -60,7 +60,6 @@ import de.mm20.launcher2.widgets.Widget
@Composable @Composable
fun WidgetItem( fun WidgetItem(
widget: Widget, widget: Widget,
appWidgetHost: AppWidgetHost,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
editMode: Boolean = false, editMode: Boolean = false,
onWidgetAdd: (widget: Widget, offset: Int) -> Unit = { _, _ -> }, onWidgetAdd: (widget: Widget, offset: Int) -> Unit = { _, _ -> },
@ -222,7 +221,6 @@ fun WidgetItem(
} }
} else { } else {
ExternalWidget( ExternalWidget(
appWidgetHost = appWidgetHost,
widgetId = widget.config.widgetId, widgetId = widget.config.widgetId,
widgetInfo = widgetInfo, widgetInfo = widgetInfo,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -237,7 +235,6 @@ fun WidgetItem(
} }
if (configure) { if (configure) {
ConfigureWidgetSheet( ConfigureWidgetSheet(
appWidgetHost = appWidgetHost,
widget = widget, widget = widget,
onWidgetUpdated = onWidgetUpdate, onWidgetUpdated = onWidgetUpdate,
onDismiss = { configure = false }, 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.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.AnalogClock 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.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.DigitalClock1
import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.DigitalClock2 import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.DigitalClock2
import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.OrbitClock import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.OrbitClock
@ -97,8 +98,11 @@ fun ClockWidget(
val alignment by viewModel.alignment.collectAsState() val alignment by viewModel.alignment.collectAsState()
val time = LocalTime.current val time = LocalTime.current
val darkColors =
color == ClockWidgetColors.Auto && LocalPreferDarkContentOverWallpaper.current || color == ClockWidgetColors.Dark
val contentColor = val contentColor =
if (color == ClockWidgetColors.Auto && LocalPreferDarkContentOverWallpaper.current || color == ClockWidgetColors.Dark) { if (darkColors) {
Color(0, 0, 0, 180) Color(0, 0, 0, 180)
} else { } else {
Color.White Color.White
@ -182,7 +186,7 @@ fun ClockWidget(
viewModel.launchClockApp(context) viewModel.launchClockApp(context)
} }
) { ) {
Clock(clockStyle, false) Clock(clockStyle, false, darkColors)
} }
if (partProvider != null) { if (partProvider != null) {
@ -227,7 +231,7 @@ fun ClockWidget(
viewModel.launchClockApp(context) 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 @Composable
fun Clock( fun Clock(
style: ClockWidgetStyle?, style: ClockWidgetStyle?,
compact: Boolean, compact: Boolean,
darkColors: Boolean = false
) { ) {
val time = LocalTime.current val time = LocalTime.current
val clockSettings: ClockWidgetSettings by inject() val clockSettings: ClockWidgetSettings by inject()
@ -264,14 +272,51 @@ fun Clock(
style, style,
compact, compact,
showSeconds, showSeconds,
useThemeColor useThemeColor,
darkColors
) )
is ClockWidgetStyle.Digital2 -> DigitalClock2(time, compact, showSeconds, useThemeColor) is ClockWidgetStyle.Digital2 -> DigitalClock2(
is ClockWidgetStyle.Binary -> BinaryClock(time, compact, showSeconds, useThemeColor) time,
is ClockWidgetStyle.Analog -> AnalogClock(time, compact, showSeconds, useThemeColor) compact,
is ClockWidgetStyle.Orbit -> OrbitClock(time, compact, showSeconds, useThemeColor) showSeconds,
is ClockWidgetStyle.Segment -> SegmentClock(time, compact, showSeconds, useThemeColor) 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 -> {} is ClockWidgetStyle.Empty -> {}
else -> {} else -> {}
} }
@ -426,7 +471,7 @@ fun ConfigureClockWidgetSheet(
viewModel.setUseThemeColor(it) viewModel.setUseThemeColor(it)
} }
) )
AnimatedVisibility(compact == false) { AnimatedVisibility(compact == false && style !is ClockWidgetStyle.Custom) {
SwitchPreference( SwitchPreference(
title = stringResource(R.string.preference_clock_widget_show_seconds), title = stringResource(R.string.preference_clock_widget_show_seconds),
icon = Icons.Rounded.AccessTime, icon = Icons.Rounded.AccessTime,

View File

@ -1,6 +1,11 @@
package de.mm20.launcher2.ui.launcher.widgets.clock 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.content.Context
import android.os.Build
import android.util.Log
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut 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.ChevronLeft
import androidx.compose.material.icons.rounded.ChevronRight import androidx.compose.material.icons.rounded.ChevronRight
import androidx.compose.material.icons.rounded.Settings 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.Tune
import androidx.compose.material.icons.rounded.Widgets
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
@ -38,6 +43,7 @@ import androidx.compose.runtime.getValue
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.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.ClockWidgetColors
import de.mm20.launcher2.preferences.ClockWidgetStyle import de.mm20.launcher2.preferences.ClockWidgetStyle
import de.mm20.launcher2.ui.R 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.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
import de.mm20.launcher2.widgets.AppWidget
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
@ -63,6 +72,9 @@ fun WatchFaceSelector(
onSelect: (ClockWidgetStyle) -> Unit, onSelect: (ClockWidgetStyle) -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
var showWidgetPicker by rememberSaveable { mutableStateOf(false) }
Surface( Surface(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -78,7 +90,8 @@ fun WatchFaceSelector(
modifier = Modifier, modifier = Modifier,
) { ) {
val pagerState = rememberPagerState( val pagerState = rememberPagerState(
initialPage = styles.indexOfFirst { it.javaClass == selected?.javaClass }.coerceAtLeast(0), initialPage = styles.indexOfFirst { it.javaClass == selected?.javaClass }
.coerceAtLeast(0),
) { ) {
styles.size styles.size
} }
@ -93,7 +106,7 @@ fun WatchFaceSelector(
Box { Box {
androidx.compose.animation.AnimatedVisibility( androidx.compose.animation.AnimatedVisibility(
selected is ClockWidgetStyle.Digital1, selected is ClockWidgetStyle.Digital1 || selected is ClockWidgetStyle.Custom,
modifier = Modifier modifier = Modifier
.align(Alignment.TopEnd) .align(Alignment.TopEnd)
.zIndex(1f), .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( CompositionLocalProvider(
LocalContentColor provides if (colors == ClockWidgetColors.Auto && LocalPreferDarkContentOverWallpaper.current || colors == ClockWidgetColors.Dark) { LocalContentColor provides if (darkColors) {
Color(0, 0, 0, 180) Color(0, 0, 0, 180)
} else { } else {
Color.White Color.White
@ -148,9 +209,9 @@ fun WatchFaceSelector(
) { ) {
val currentPageStyle = styles[pageIndex] val currentPageStyle = styles[pageIndex]
if (currentPageStyle.javaClass == selected?.javaClass) { if (currentPageStyle.javaClass == selected?.javaClass) {
Clock(selected, compact) Clock(selected, compact, darkColors)
} else { } 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 { 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.Analog -> context.getString(R.string.clock_style_analog)
is ClockWidgetStyle.Segment -> context.getString(R.string.clock_style_segment) is ClockWidgetStyle.Segment -> context.getString(R.string.clock_style_segment)
is ClockWidgetStyle.Empty -> context.getString(R.string.clock_style_empty) is ClockWidgetStyle.Empty -> context.getString(R.string.clock_style_empty)
is ClockWidgetStyle.Custom -> context.getString(R.string.clock_style_custom)
else -> "" 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, compact: Boolean,
showSeconds: Boolean, showSeconds: Boolean,
useThemeColor: Boolean, useThemeColor: Boolean,
darkColors: Boolean,
) { ) {
val verticalLayout = !compact val verticalLayout = !compact
val date = Calendar.getInstance() val date = Calendar.getInstance()
@ -34,7 +35,7 @@ fun AnalogClock(
val strokeWidth = if (verticalLayout) 4.dp else 2.dp val strokeWidth = if (verticalLayout) 4.dp else 2.dp
val color = if (useThemeColor) { val color = if (useThemeColor) {
if (LocalContentColor.current == Color.White) { if (!darkColors) {
if (LocalDarkTheme.current) MaterialTheme.colorScheme.onPrimaryContainer if (LocalDarkTheme.current) MaterialTheme.colorScheme.onPrimaryContainer
else MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.primaryContainer
} else { } else {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,48 +1,53 @@
package de.mm20.launcher2.ui.launcher.widgets.external package de.mm20.launcher2.ui.launcher.widgets.external
import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo import android.appwidget.AppWidgetProviderInfo
import android.os.Bundle import android.os.Build
import android.util.Log import android.util.SparseIntArray
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ListView import android.widget.ListView
import android.widget.ScrollView import android.widget.ScrollView
import androidx.appcompat.app.AppCompatActivity import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height 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.Composable
import androidx.compose.runtime.key import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp 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.iterator import androidx.core.view.iterator
import androidx.core.view.setPadding 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 de.mm20.launcher2.ui.ktx.toPixels
import palettes.TonalPalette
import kotlin.math.roundToInt import kotlin.math.roundToInt
@Composable @Composable
fun ExternalWidget( fun ExternalWidget(
appWidgetHost: AppWidgetHost,
widgetInfo: AppWidgetProviderInfo, widgetInfo: AppWidgetProviderInfo,
widgetId: Int, widgetId: Int,
height: Int, height: Int,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
borderless: Boolean = false, borderless: Boolean = false,
useThemeColors: Boolean = false,
onLightBackground: Boolean = false,
) { ) {
val padding = if (borderless) 0 else 8.dp.toPixels().roundToInt() val padding = if (borderless) 0 else 8.dp.toPixels().roundToInt()
val colorScheme = MaterialTheme.colorScheme
val appWidgetHost = LocalAppWidgetHost.current
BoxWithConstraints { BoxWithConstraints {
val maxWidth = maxWidth val maxWidth = maxWidth
key(widgetId) { key(widgetId) {
AndroidView( AndroidView(
modifier = modifier modifier = modifier
.fillMaxWidth()
.height(height.dp), .height(height.dp),
factory = { factory = {
val view = appWidgetHost.createView(it.applicationContext, widgetId, widgetInfo) val view = appWidgetHost.createView(it.applicationContext, widgetId, widgetInfo)
@ -50,6 +55,19 @@ fun ExternalWidget(
return@AndroidView view return@AndroidView view
}, },
update = { update = {
if (isAtLeastApiLevel(31)) {
if (useThemeColors) {
val colorMapping = getColorMapping(colorScheme)
it.setColorResources(colorMapping)
} else {
it.resetColorResources()
}
}
if (isAtLeastApiLevel(29)) {
it.setOnLightBackground(onLightBackground)
}
it.updateAppWidgetSize( it.updateAppWidgetSize(
null, null,
maxWidth.value.roundToInt(), maxWidth.value.roundToInt(),
@ -71,4 +89,77 @@ private fun enableNestedScroll(view: View) {
} }
} }
if (view is ListView || view is ScrollView) view.isNestedScrollingEnabled = true 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 LocalWindowSize = compositionLocalOf { Size(0f, 0f) }
val LocalAppWidgetHost = compositionLocalOf<AppWidgetHost?>(defaultFactory = { null })
val LocalNavController = compositionLocalOf<NavController?> { null } val LocalNavController = compositionLocalOf<NavController?> { null }
val LocalCardStyle = compositionLocalOf { CardStyle() } 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.AppLicense
import de.mm20.launcher2.licenses.OpenSourceLicenses import de.mm20.launcher2.licenses.OpenSourceLicenses
import de.mm20.launcher2.ui.base.BaseActivity import de.mm20.launcher2.ui.base.BaseActivity
import de.mm20.launcher2.ui.base.ProvideCurrentTime import de.mm20.launcher2.ui.base.ProvideCompositionLocals
import de.mm20.launcher2.ui.base.ProvideSettings
import de.mm20.launcher2.ui.locals.LocalDarkTheme import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalNavController import de.mm20.launcher2.ui.locals.LocalNavController
import de.mm20.launcher2.ui.locals.LocalWallpaperColors import de.mm20.launcher2.ui.locals.LocalWallpaperColors
@ -84,158 +83,161 @@ class SettingsActivity : BaseActivity() {
LocalNavController provides navController, LocalNavController provides navController,
LocalWallpaperColors provides wallpaperColors, LocalWallpaperColors provides wallpaperColors,
) { ) {
ProvideSettings { ProvideCompositionLocals {
ProvideCurrentTime { LauncherTheme {
LauncherTheme { val systemBarColor = MaterialTheme.colorScheme.surfaceDim
val systemBarColor = MaterialTheme.colorScheme.surfaceDim val systemBarColorAlt = MaterialTheme.colorScheme.onSurface
val systemBarColorAlt = MaterialTheme.colorScheme.onSurface val isDarkTheme = LocalDarkTheme.current
val isDarkTheme = LocalDarkTheme.current LaunchedEffect(isDarkTheme, systemBarColor, systemBarColorAlt) {
LaunchedEffect(isDarkTheme, systemBarColor, systemBarColorAlt) { enableEdgeToEdge(
enableEdgeToEdge( if (isDarkTheme) SystemBarStyle.dark(systemBarColor.toArgb())
if (isDarkTheme) SystemBarStyle.dark(systemBarColor.toArgb()) else SystemBarStyle.light(
else SystemBarStyle.light(systemBarColor.toArgb(), systemBarColorAlt.toArgb()) systemBarColor.toArgb(),
systemBarColorAlt.toArgb()
) )
} )
OverlayHost { }
NavHost( OverlayHost {
navController = navController, NavHost(
startDestination = "settings", navController = navController,
exitTransition = { startDestination = "settings",
fadeOut() + scaleOut(targetScale = 0.5f) exitTransition = {
}, fadeOut() + scaleOut(targetScale = 0.5f)
enterTransition = { },
slideInHorizontally { it } enterTransition = {
}, slideInHorizontally { it }
popEnterTransition = { },
fadeIn() + scaleIn(initialScale = 0.5f) popEnterTransition = {
}, fadeIn() + scaleIn(initialScale = 0.5f)
popExitTransition = { },
slideOutHorizontally { it } 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") { val id = it.arguments?.getString("id")?.let {
MainSettingsScreen() UUID.fromString(it)
} } ?: return@composable
composable("settings/appearance") { ThemeSettingsScreen(id)
AppearanceSettingsScreen() }
} composable("settings/appearance/cards") {
composable("settings/homescreen") { CardsSettingsScreen()
HomescreenSettingsScreen() }
} composable("settings/search") {
composable("settings/icons") { SearchSettingsScreen()
IconsSettingsScreen() }
} composable("settings/gestures") {
composable("settings/appearance/themes") { GestureSettingsScreen()
ThemesSettingsScreen() }
} composable("settings/search/unitconverter") {
composable( UnitConverterSettingsScreen()
"settings/appearance/themes/{id}", }
arguments = listOf(navArgument("id") { composable("settings/search/wikipedia") {
nullable = false WikipediaSettingsScreen()
}) }
) { composable("settings/search/locations") {
val id = it.arguments?.getString("id")?.let { LocationsSettingsScreen()
UUID.fromString(it) }
} ?: return@composable composable("settings/search/files") {
ThemeSettingsScreen(id) FileSearchSettingsScreen()
} }
composable("settings/appearance/cards") { composable("settings/search/searchactions") {
CardsSettingsScreen() SearchActionsSettingsScreen()
} }
composable("settings/search") { composable("settings/search/hiddenitems") {
SearchSettingsScreen() HiddenItemsSettingsScreen()
} }
composable("settings/gestures") { composable("settings/search/tags") {
GestureSettingsScreen() TagsSettingsScreen()
} }
composable("settings/search/unitconverter") { composable(ROUTE_WEATHER_INTEGRATION) {
UnitConverterSettingsScreen() WeatherIntegrationSettingsScreen()
} }
composable("settings/search/wikipedia") { composable(ROUTE_MEDIA_INTEGRATION) {
WikipediaSettingsScreen() MediaIntegrationSettingsScreen()
} }
composable("settings/search/locations") { composable("settings/favorites") {
LocationsSettingsScreen() FavoritesSettingsScreen()
} }
composable("settings/search/files") { composable("settings/integrations") {
FileSearchSettingsScreen() IntegrationsSettingsScreen()
} }
composable("settings/search/searchactions") { composable("settings/plugins") {
SearchActionsSettingsScreen() PluginsSettingsScreen()
} }
composable("settings/search/hiddenitems") { composable("settings/plugins/{id}") {
HiddenItemsSettingsScreen() PluginSettingsScreen(
} it.arguments?.getString("id") ?: return@composable
composable("settings/search/tags") { )
TagsSettingsScreen() }
} composable("settings/about") {
composable(ROUTE_WEATHER_INTEGRATION) { AboutSettingsScreen()
WeatherIntegrationSettingsScreen() }
} composable("settings/about/buildinfo") {
composable(ROUTE_MEDIA_INTEGRATION) { BuildInfoSettingsScreen()
MediaIntegrationSettingsScreen() }
} composable("settings/about/easteregg") {
composable("settings/favorites") { EasterEggSettingsScreen()
FavoritesSettingsScreen() }
} composable("settings/debug") {
composable("settings/integrations") { DebugSettingsScreen()
IntegrationsSettingsScreen() }
} composable("settings/backup") {
composable("settings/plugins") { BackupSettingsScreen()
PluginsSettingsScreen() }
} composable("settings/debug/crashreporter") {
composable("settings/plugins/{id}") { CrashReporterScreen()
PluginSettingsScreen(it.arguments?.getString("id") ?: return@composable) }
} composable("settings/debug/logs") {
composable("settings/about") { LogScreen()
AboutSettingsScreen() }
} composable(
composable("settings/about/buildinfo") { "settings/debug/crashreporter/report?fileName={fileName}",
BuildInfoSettingsScreen() arguments = listOf(navArgument("fileName") {
} nullable = false
composable("settings/about/easteregg") { })
EasterEggSettingsScreen() ) {
} val fileName = it.arguments?.getString("fileName")
composable("settings/debug") { ?.let {
DebugSettingsScreen() URLDecoder.decode(it, "utf8")
} }
composable("settings/backup") { CrashReportScreen(fileName!!)
BackupSettingsScreen() }
} composable(
composable("settings/debug/crashreporter") { "settings/license?library={libraryName}",
CrashReporterScreen() arguments = listOf(navArgument("libraryName") {
} nullable = true
composable("settings/debug/logs") { })
LogScreen() ) {
} val libraryName = it.arguments?.getString("libraryName")
composable( val library = remember(libraryName) {
"settings/debug/crashreporter/report?fileName={fileName}", if (libraryName == null) {
arguments = listOf(navArgument("fileName") { AppLicense.get(this@SettingsActivity)
nullable = false } else {
}) OpenSourceLicenses.first { it.name == libraryName }
) {
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.ClockWidgetStyle
import de.mm20.launcher2.preferences.ui.ClockWidgetSettings import de.mm20.launcher2.preferences.ui.ClockWidgetSettings
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@ -20,7 +21,7 @@ class ClockWidgetSettingsScreenVM : ViewModel(), KoinComponent {
settings.setCompact(compact) settings.setCompact(compact)
} }
val availableClockStyles = settings.digital1.map {digital1 -> val availableClockStyles = combine(settings.digital1, settings.custom) {digital1, custom ->
listOf( listOf(
digital1, digital1,
ClockWidgetStyle.Digital2, ClockWidgetStyle.Digital2,
@ -28,6 +29,7 @@ class ClockWidgetSettingsScreenVM : ViewModel(), KoinComponent {
ClockWidgetStyle.Orbit, ClockWidgetStyle.Orbit,
ClockWidgetStyle.Segment, ClockWidgetStyle.Segment,
ClockWidgetStyle.Binary, ClockWidgetStyle.Binary,
custom,
ClockWidgetStyle.Empty, ClockWidgetStyle.Empty,
) )
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) }.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_appwidget_resize_hint">Drag to resize</string>
<string name="widget_config_weather_integration_settings">Weather integration settings</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_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="app_widget_loading_failed">App widget failed to load.</string>
<string name="widget_config_music_integration_settings">Media control integration settings</string> <string name="widget_config_music_integration_settings">Media control integration settings</string>
<string name="note_widget_link_file">Link to file</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_analog">Hands</string>
<string name="clock_style_segment">7-segment</string> <string name="clock_style_segment">7-segment</string>
<string name="clock_style_empty">No clock</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_standard">Standard</string>
<string name="clock_variant_outlined">Outlined</string> <string name="clock_variant_outlined">Outlined</string>
</resources> </resources>

View File

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

View File

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