From eea7019ca9479a61dd047f5b23bc88d8dbb66856 Mon Sep 17 00:00:00 2001 From: Tommy Carozzani <27284007+orgonth@users.noreply.github.com> Date: Mon, 24 Feb 2025 20:27:05 +0100 Subject: [PATCH] Add customizable action on clock widget tap --- .../ui/launcher/widgets/clock/ClockWidget.kt | 30 ++++++++++++ .../launcher/widgets/clock/ClockWidgetVM.kt | 46 +++++++++++++++++-- .../ClockWidgetSettingsScreenVM.kt | 37 +++++++++++++++ core/i18n/src/main/res/values/strings.xml | 2 + .../preferences/LauncherSettingsData.kt | 5 ++ .../preferences/ui/ClockWidgetSettings.kt | 10 ++++ 6 files changed, 127 insertions(+), 3 deletions(-) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidget.kt index 2416990c9..5ad5de021 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidget.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidget.kt @@ -1,5 +1,6 @@ package de.mm20.launcher2.ui.launcher.widgets.clock +import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background @@ -65,16 +66,20 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import de.mm20.launcher2.ktx.isAtLeastApiLevel import de.mm20.launcher2.preferences.ClockWidgetAlignment import de.mm20.launcher2.preferences.ClockWidgetColors import de.mm20.launcher2.preferences.ClockWidgetStyle +import de.mm20.launcher2.preferences.GestureAction import de.mm20.launcher2.preferences.TimeFormat import de.mm20.launcher2.preferences.ui.ClockWidgetSettings import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.base.LocalTime import de.mm20.launcher2.ui.component.BottomSheetDialog +import de.mm20.launcher2.ui.component.MissingPermissionBanner import de.mm20.launcher2.ui.component.preferences.Preference import de.mm20.launcher2.ui.component.preferences.SwitchPreference +import de.mm20.launcher2.ui.ktx.toPixels 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 @@ -85,6 +90,8 @@ import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.SegmentClock import de.mm20.launcher2.ui.launcher.widgets.clock.parts.PartProvider import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper import de.mm20.launcher2.ui.settings.clockwidget.ClockWidgetSettingsScreenVM +import de.mm20.launcher2.ui.settings.gestures.GesturePreference +import de.mm20.launcher2.ui.settings.gestures.requiresAccessibilityService import de.mm20.launcher2.ui.utils.isTwentyFourHours import org.koin.androidx.compose.inject @@ -576,6 +583,29 @@ fun ConfigureClockWidgetSheet( viewModel.setFillHeight(it) } ) + + val options = buildList { + add(stringResource(R.string.gesture_action_none) to GestureAction.NoAction) + add(stringResource(R.string.gesture_action_alarms) to GestureAction.Alarms) + add(stringResource(R.string.gesture_action_launch_app) to GestureAction.Launch(null)) + } + val appIconSize = 32.dp.toPixels() + val tapAction by viewModel.tapAction.collectAsStateWithLifecycle(null) + val tapApp by viewModel.tapApp.collectAsState(null) + val tapAppIcon by remember(tapApp?.key) { + viewModel.getIcon(tapApp, appIconSize.toInt()) + }.collectAsState(null) + GesturePreference( + title = stringResource(R.string.preference_gesture_tap), + value = tapAction, + onValueChanged = { viewModel.setTapAction(it) }, + isOpenSearch = false, + options = options, + app = tapApp, + appIcon = tapAppIcon, + onAppChanged = { viewModel.setTapApp(it) } + ) + AnimatedVisibility(fillHeight == true) { var showDropdown by remember { mutableStateOf(false) } Preference( diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidgetVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidgetVM.kt index f1a59ac5e..226ed9fbb 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidgetVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidgetVM.kt @@ -1,12 +1,17 @@ package de.mm20.launcher2.ui.launcher.widgets.clock +import android.app.Activity import android.content.Context import android.content.Intent import android.provider.AlarmClock +import androidx.core.app.ActivityOptionsCompat import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import de.mm20.launcher2.ktx.tryStartActivity +import de.mm20.launcher2.preferences.GestureAction import de.mm20.launcher2.preferences.ui.ClockWidgetSettings +import de.mm20.launcher2.search.SavableSearchable +import de.mm20.launcher2.searchable.SavableSearchableRepository import de.mm20.launcher2.ui.launcher.widgets.clock.parts.AlarmPartProvider import de.mm20.launcher2.ui.launcher.widgets.clock.parts.BatteryPartProvider import de.mm20.launcher2.ui.launcher.widgets.clock.parts.DatePartProvider @@ -15,9 +20,12 @@ import de.mm20.launcher2.ui.launcher.widgets.clock.parts.MusicPartProvider import de.mm20.launcher2.ui.launcher.widgets.clock.parts.PartProvider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import org.koin.core.component.KoinComponent @@ -25,6 +33,7 @@ import org.koin.core.component.inject class ClockWidgetVM : ViewModel(), KoinComponent { private val settings: ClockWidgetSettings by inject() + private val searchableRepository: SavableSearchableRepository by inject() private val partProviders = settings.parts.map { val providers = mutableListOf() @@ -69,9 +78,40 @@ class ClockWidgetVM : ViewModel(), KoinComponent { partProviders.value.forEach { it.setTime(time) } } + private val tapAction = settings.tapAction + .stateIn(viewModelScope, SharingStarted.Eagerly, null) + + private val tapApp: StateFlow = tapAction + .flatMapLatest { + if (it !is GestureAction.Launch || it.key == null) flowOf(null) + else searchableRepository.getByKeys(listOf(it.key!!)).map { + it.firstOrNull() + } + } + .stateIn(viewModelScope, SharingStarted.Eagerly, null) + fun launchClockApp(context: Context) { - context.tryStartActivity(Intent(AlarmClock.ACTION_SHOW_ALARMS).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - }) + when(tapAction.value) { + is GestureAction.Alarms -> { + context.tryStartActivity(Intent(AlarmClock.ACTION_SHOW_ALARMS).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + }) + } + + is GestureAction.Launch -> { + val view = (context as Activity).window.decorView + val options = ActivityOptionsCompat.makeScaleUpAnimation( + view, + 0, + 0, + view.width, + view.height + ) + tapApp.value?.launch(context, options.toBundle()) + } + + else -> {} + } } + } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/clockwidget/ClockWidgetSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/clockwidget/ClockWidgetSettingsScreenVM.kt index e123b7f4b..27b4fd90f 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/clockwidget/ClockWidgetSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/clockwidget/ClockWidgetSettingsScreenVM.kt @@ -2,13 +2,22 @@ package de.mm20.launcher2.ui.settings.clockwidget import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import de.mm20.launcher2.icons.IconService +import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.preferences.ClockWidgetAlignment import de.mm20.launcher2.preferences.ClockWidgetColors import de.mm20.launcher2.preferences.ClockWidgetStyle +import de.mm20.launcher2.preferences.GestureAction import de.mm20.launcher2.preferences.TimeFormat import de.mm20.launcher2.preferences.ui.ClockWidgetSettings +import de.mm20.launcher2.search.SavableSearchable +import de.mm20.launcher2.searchable.SavableSearchableRepository +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import org.koin.core.component.KoinComponent @@ -16,6 +25,9 @@ import org.koin.core.component.inject class ClockWidgetSettingsScreenVM : ViewModel(), KoinComponent { private val settings: ClockWidgetSettings by inject() + private val searchableRepository: SavableSearchableRepository by inject() + private val iconService: IconService by inject() + val compact = settings.compact .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) fun setCompact(compact: Boolean) { @@ -75,6 +87,26 @@ class ClockWidgetSettingsScreenVM : ViewModel(), KoinComponent { settings.setFillHeight(fillHeight) } + val tapAction = settings.tapAction + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) + fun setTapAction(action: GestureAction) { + settings.setTapAction(action) + } + + val tapApp: Flow = tapAction + .flatMapLatest { + if (it !is GestureAction.Launch || it.key == null) flowOf(null) + else searchableRepository.getByKeys(listOf(it.key!!)).map { + it.firstOrNull() + } + } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis = 10000), null) + + fun setTapApp(searchable: SavableSearchable?) { + searchable?.let { searchableRepository.insert(it) } ?: return + setTapAction(GestureAction.Launch(searchable.key)) + } + val parts = settings.parts .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) @@ -100,4 +132,9 @@ class ClockWidgetSettingsScreenVM : ViewModel(), KoinComponent { fun setAlignment(alignment: ClockWidgetAlignment) { settings.setAlignment(alignment) } + + fun getIcon(searchable: SavableSearchable?, size: Int): Flow { + if (searchable == null) return emptyFlow() + return iconService.getIcon(searchable, size) + } } \ No newline at end of file diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index 8def40236..2a64338a8 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -764,8 +764,10 @@ Swipe left Swipe right Double tap + Tap Long press Home button/gesture + Show alarms Do nothing Open search Launch app diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt index ede1fec08..84cd02480 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt @@ -42,6 +42,7 @@ data class LauncherSettingsData internal constructor( val clockWidgetDatePart: Boolean = true, val clockWidgetFillHeight: Boolean = true, val clockWidgetAlignment: ClockWidgetAlignment = ClockWidgetAlignment.Bottom, + val clockTapAction: GestureAction = GestureAction.Alarms, val homeScreenDock: Boolean = false, @@ -373,6 +374,10 @@ sealed interface GestureAction { @Serializable @SerialName("launch_searchable") data class Launch(val key: String?) : GestureAction + + @Serializable + @SerialName("alarms") + data object Alarms : GestureAction } diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/ClockWidgetSettings.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/ClockWidgetSettings.kt index d12094e6f..a117dad66 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/ClockWidgetSettings.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/ClockWidgetSettings.kt @@ -4,6 +4,7 @@ import de.mm20.launcher2.preferences.ClockWidgetAlignment import de.mm20.launcher2.preferences.ClockWidgetColors import de.mm20.launcher2.preferences.ClockWidgetStyle import de.mm20.launcher2.preferences.ClockWidgetStyleEnum +import de.mm20.launcher2.preferences.GestureAction import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.TimeFormat import kotlinx.coroutines.flow.Flow @@ -72,6 +73,15 @@ class ClockWidgetSettings internal constructor( } } + val tapAction: Flow = launcherDataStore.data.map { it.clockTapAction } + .distinctUntilChanged() + + fun setTapAction(action: GestureAction) { + launcherDataStore.update { + it.copy(clockTapAction = action) + } + } + val dock get() = launcherDataStore.data.map { it.homeScreenDock }