Merge remote-tracking branch 'upstream/main' into feat/materialYouCompat

Signed-off-by: Guillermo Villafuerte <gvillafu@comunidad.unam.mx>

# Conflicts:
#	app/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreen.kt
#	app/ui/src/main/java/de/mm20/launcher2/ui/theme/colorscheme/Custom.kt
#	core/preferences/src/main/java/de/mm20/launcher2/preferences/DataStore.kt
#	core/preferences/src/main/java/de/mm20/launcher2/preferences/Defaults.kt
This commit is contained in:
Guillermo Villafuerte
2024-01-19 18:10:42 -06:00
211 changed files with 4394 additions and 3229 deletions
@@ -19,19 +19,17 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.SearchBarColors
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.ui.component.SearchBarLevel
import de.mm20.launcher2.ui.launcher.LauncherScaffoldVM
import de.mm20.launcher2.ui.launcher.gestures.LauncherGestureHandler
import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur
import de.mm20.launcher2.ui.launcher.search.SearchColumn
import de.mm20.launcher2.ui.launcher.search.SearchVM
import de.mm20.launcher2.ui.launcher.searchbar.LauncherSearchBar
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
@Composable
fun AssistantScaffold(
@@ -204,7 +202,7 @@ fun AssistantScaffold(
showHiddenItemsButton = true,
value = { value },
onValueChange = { searchVM.search(it) },
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == Settings.SearchBarSettings.SearchBarColors.Auto || searchBarColor == Settings.SearchBarSettings.SearchBarColors.Dark,
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark,
style = searchBarStyle,
reverse = bottomSearchBar,
onKeyboardActionGo = if (launchOnEnter) {
@@ -1,9 +1,11 @@
package de.mm20.launcher2.ui.base
import androidx.compose.runtime.*
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.IconShape
import de.mm20.launcher2.preferences.LegacySettings
import de.mm20.launcher2.preferences.ui.CardStyle
import de.mm20.launcher2.preferences.ui.GridSettings
import de.mm20.launcher2.preferences.ui.UiSettings
import de.mm20.launcher2.ui.component.ProvideIconShape
import de.mm20.launcher2.ui.locals.LocalCardStyle
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
@@ -19,32 +21,28 @@ import org.koin.androidx.compose.inject
fun ProvideSettings(
content: @Composable () -> Unit
) {
val dataStore: LauncherDataStore by inject()
val settings: UiSettings by inject()
val widgetRepository: WidgetRepository by inject()
val cardStyle by remember {
dataStore.data.map { it.cards }.distinctUntilChanged()
settings.cardStyle.distinctUntilChanged()
}.collectAsState(
Settings.CardSettings.getDefaultInstance()
CardStyle()
)
val iconShape by remember {
dataStore.data.map {
if (it.easterEgg) Settings.IconSettings.IconShape.EasterEgg
else it.icons.shape
}.distinctUntilChanged()
}.collectAsState(Settings.IconSettings.IconShape.Circle)
settings.iconShape.distinctUntilChanged()
}.collectAsState(IconShape.Circle)
val favoritesEnabled by remember {
combine(
widgetRepository.exists(FavoritesWidget.Type),
dataStore.data.map { it.favorites.enabled },
dataStore.data.map { it.clockWidget.favoritesPart },
) { a, b, c -> a || b || c }.distinctUntilChanged()
settings.favoritesEnabled,
) { a, b -> a || b }.distinctUntilChanged()
}.collectAsState(true)
val gridSettings by remember {
dataStore.data.map { it.grid }.distinctUntilChanged()
}.collectAsState(Settings.GridSettings.newBuilder().setColumnCount(5).setShowLabels(true).setIconSize(48).build())
settings.gridSettings.distinctUntilChanged()
}.collectAsState(GridSettings())
CompositionLocalProvider(
LocalCardStyle provides cardStyle,
@@ -4,7 +4,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
import de.mm20.launcher2.data.customattrs.utils.withCustomLabels
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.search.FavoritesSettings
import de.mm20.launcher2.preferences.search.FavoritesSettingsData
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.Tag
import de.mm20.launcher2.services.favorites.FavoritesService
@@ -19,11 +20,11 @@ abstract class FavoritesVM : ViewModel(), KoinComponent {
private val favoritesService: FavoritesService by inject()
internal val widgetRepository: WidgetRepository by inject()
private val customAttributesRepository: CustomAttributesRepository by inject()
internal val dataStore: LauncherDataStore by inject()
internal val settings: FavoritesSettings by inject()
val selectedTag = MutableStateFlow<String?>(null)
val showEditButton = dataStore.data.map { it.favorites.editButton }
val showEditButton = settings.showEditButton
abstract val tagsExpanded: Flow<Boolean>
val pinnedTags = favoritesService.getFavorites(
@@ -36,24 +37,18 @@ abstract class FavoritesVM : ViewModel(), KoinComponent {
open val favorites: Flow<List<SavableSearchable>> = selectedTag.flatMapLatest { tag ->
if (tag == null) {
val columns = dataStore.data.map { it.grid.columnCount }
val excludeCalendar = widgetRepository.exists(CalendarWidget.Type)
val includeFrequentlyUsed = dataStore.data.map { it.favorites.frequentlyUsed }
val frequentlyUsedRows = dataStore.data.map { it.favorites.frequentlyUsedRows }
combine(
listOf(
columns,
excludeCalendar,
includeFrequentlyUsed,
frequentlyUsedRows
)
) { it }.transformLatest {
excludeCalendar,
settings,
) { (a, b) -> a as Boolean to b as FavoritesSettingsData }
.transformLatest {
val columns = it[0] as Int
val excludeCalendar = it[1] as Boolean
val includeFrequentlyUsed = it[2] as Boolean
val frequentlyUsedRows = it[3] as Int
val columns = it.second.columns
val excludeCalendar = it.first
val includeFrequentlyUsed = it.second.frequentlyUsed
val frequentlyUsedRows = it.second.frequentlyUsedRows
val pinned = favoritesService.getFavorites(
excludeTypes = if (excludeCalendar) listOf("calendar", "tag") else listOf("tag"),
@@ -5,7 +5,8 @@ import android.net.Uri
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.ThemeDescriptor
import de.mm20.launcher2.preferences.ui.UiSettings
import de.mm20.launcher2.themes.Theme
import de.mm20.launcher2.themes.ThemeRepository
import de.mm20.launcher2.themes.fromJson
@@ -17,7 +18,7 @@ import org.koin.core.component.inject
class ImportThemeSheetVM : ViewModel(), KoinComponent {
private val themeRepository: ThemeRepository by inject()
private val dataStore: LauncherDataStore by inject()
private val uiSettings: UiSettings by inject()
val theme = mutableStateOf<Theme?>(null)
val error = mutableStateOf<Boolean>(false)
@@ -56,16 +57,10 @@ class ImportThemeSheetVM : ViewModel(), KoinComponent {
}
}
private suspend fun importTheme(theme: Theme, apply: Boolean) {
private fun importTheme(theme: Theme, apply: Boolean) {
themeRepository.createTheme(theme)
if (apply) {
dataStore.updateData {
it.toBuilder()
.setAppearance(
it.appearance.toBuilder()
.setThemeId(theme.id.toString())
).build()
}
uiSettings.setTheme(ThemeDescriptor.Custom(theme.id.toString()))
}
}
}
@@ -7,7 +7,6 @@ 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.LauncherDataStore
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchService
import de.mm20.launcher2.search.toList
@@ -15,15 +14,13 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class SearchablePickerVM: ViewModel(), KoinComponent {
class SearchablePickerVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val searchService: SearchService by inject()
private val iconService: IconService by inject()
@@ -41,15 +38,11 @@ class SearchablePickerVM: ViewModel(), KoinComponent {
searchQuery = query
searchJob?.cancel()
searchJob = viewModelScope.launch {
val settings = dataStore.data.first()
searchService.search(
query = query,
shortcuts = settings.appShortcutSearch,
contacts = settings.contactsSearch,
calendars = settings.calendarSearch,
).collectLatest {
if (searchQuery != query) return@collectLatest
items = withContext(Dispatchers.Default) {
items = withContext(Dispatchers.Default) {
it.toList().filterIsInstance<SavableSearchable>().sorted()
}
}
@@ -2,9 +2,9 @@ package de.mm20.launcher2.ui.common
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.weather.WeatherLocation
import de.mm20.launcher2.preferences.weather.WeatherLocation
import de.mm20.launcher2.preferences.weather.WeatherSettings
import de.mm20.launcher2.weather.WeatherRepository
import de.mm20.launcher2.weather.settings.WeatherSettings
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.first
import org.koin.core.component.KoinComponent
@@ -39,7 +39,7 @@ import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.SearchBarStyle
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.layout.BottomReversed
import de.mm20.launcher2.ui.locals.LocalCardStyle
@@ -47,7 +47,7 @@ import de.mm20.launcher2.ui.locals.LocalCardStyle
@Composable
fun SearchBar(
modifier: Modifier = Modifier,
style: Settings.SearchBarSettings.SearchBarStyle,
style: SearchBarStyle,
level: SearchBarLevel,
value: String,
onValueChange: (String) -> Unit,
@@ -78,7 +78,7 @@ fun SearchBar(
}
) {
when {
it == SearchBarLevel.Resting && style != Settings.SearchBarSettings.SearchBarStyle.Solid -> 0.dp
it == SearchBarLevel.Resting && style != SearchBarStyle.Solid -> 0.dp
it == SearchBarLevel.Raised -> 8.dp
else -> 2.dp
}
@@ -98,7 +98,7 @@ fun SearchBar(
}) {
when {
it == SearchBarLevel.Active -> LocalCardStyle.current.opacity
style != Settings.SearchBarSettings.SearchBarStyle.Transparent -> 1f
style != SearchBarStyle.Transparent -> 1f
it == SearchBarLevel.Resting -> 0f
else -> 1f
}
@@ -117,14 +117,14 @@ fun SearchBar(
}
}) {
when {
style != Settings.SearchBarSettings.SearchBarStyle.Transparent -> MaterialTheme.colorScheme.onSurface
style != SearchBarStyle.Transparent -> MaterialTheme.colorScheme.onSurface
it == SearchBarLevel.Resting -> if (darkColors) Color(0, 0, 0, 180) else Color.White
else -> MaterialTheme.colorScheme.onSurface
}
}
val opacity by transition.animateFloat(label = "opacity") {
if (style == Settings.SearchBarSettings.SearchBarStyle.Hidden && it == SearchBarLevel.Resting) 0f
if (style == SearchBarStyle.Hidden && it == SearchBarLevel.Resting) 0f
else 1f
}
@@ -15,7 +15,6 @@ import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
@@ -57,7 +56,6 @@ import androidx.compose.ui.graphics.drawscope.withTransform
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.toAndroidRect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Density
@@ -80,7 +78,7 @@ import de.mm20.launcher2.icons.TintedClockLayer
import de.mm20.launcher2.icons.TintedIconLayer
import de.mm20.launcher2.icons.TransparentLayer
import de.mm20.launcher2.ktx.drawWithColorFilter
import de.mm20.launcher2.preferences.Settings.IconSettings.IconShape
import de.mm20.launcher2.preferences.IconShape
import de.mm20.launcher2.ui.base.LocalTime
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.locals.LocalDarkTheme
@@ -495,7 +493,6 @@ fun getShape(iconShape: IconShape): Shape {
IconShape.Teardrop -> TeardropShape
IconShape.Pebble -> PebbleShape
IconShape.EasterEgg -> EasterEggShape
IconShape.UNRECOGNIZED -> CircleShape
}
}
@@ -12,7 +12,6 @@ import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.*
import androidx.compose.ui.zIndex
import de.mm20.launcher2.ui.ktx.animateTo
@@ -178,7 +177,6 @@ fun LazyVerticalDragAndDropGrid(
columns,
modifier.dragAndDrop(
state,
LocalLayoutDirection.current == LayoutDirection.Rtl,
LocalHapticFeedback.current
),
state.gridState,
@@ -208,7 +206,6 @@ fun LazyHorizontalDragAndDropGrid(
rows,
modifier.dragAndDrop(
state,
LocalLayoutDirection.current == LayoutDirection.Rtl,
LocalHapticFeedback.current
),
state.gridState,
@@ -224,7 +221,6 @@ fun LazyHorizontalDragAndDropGrid(
fun Modifier.dragAndDrop(
state: LazyDragAndDropGridState,
isRtl: Boolean,
hapticFeedback: HapticFeedback
) =
this then pointerInput(null) {
@@ -235,10 +231,7 @@ fun Modifier.dragAndDrop(
onDragStart = { offset ->
val draggedItem = state.gridState.layoutInfo.visibleItemsInfo.find {
Rect(
it.offset.toOffset().let {off ->
if (isRtl) off.copy(x = state.gridState.layoutInfo.viewportSize.width - off.x - it.size.width)
else off
},
it.offset.toOffset(),
it.size.toSize()
).contains(offset)
}
@@ -250,10 +243,7 @@ fun Modifier.dragAndDrop(
val absPosition = state.draggedItemAbsolutePosition
val draggedItem = state.draggedItem
if (absPosition != null && draggedItem != null) {
state.draggedItemAbsolutePosition = absPosition + dragAmount.let {
if (isRtl) it.copy(x = -it.x)
else it
}
state.draggedItemAbsolutePosition = absPosition + dragAmount
val draggedCenter = Rect(absPosition, draggedItem.size.toSize()).center
val dragOver = state.gridState.layoutInfo.visibleItemsInfo.find {
Rect(
@@ -308,7 +298,7 @@ fun LazyGridItemScope.DraggableItem(
modifier = modifier
.then(if (isDragged) Modifier else Modifier.animateItemPlacement())
.zIndex(if (isDragged) 1f else 0f)
.offset {
.absoluteOffset {
if (state.draggedItem?.key == key) {
state.draggedItemOffset?.toIntOffset() ?: IntOffset.Zero
} else if (state.droppedItemKey == key) {
@@ -12,17 +12,20 @@ import de.mm20.launcher2.searchable.SavableSearchableRepository
import de.mm20.launcher2.globalactions.GlobalActionsService
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.Settings.GestureSettings.GestureAction
import de.mm20.launcher2.preferences.Settings.LayoutSettings.Layout
import de.mm20.launcher2.preferences.BaseLayout
import de.mm20.launcher2.preferences.ColorScheme
import de.mm20.launcher2.preferences.GestureAction
import de.mm20.launcher2.preferences.ScreenOrientation
import de.mm20.launcher2.preferences.SearchBarColors
import de.mm20.launcher2.preferences.SearchBarStyle
import de.mm20.launcher2.preferences.ui.GestureSettings
import de.mm20.launcher2.preferences.ui.UiSettings
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.gestures.Gesture
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
@@ -32,7 +35,8 @@ import org.koin.core.component.inject
class LauncherScaffoldVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val uiSettings: UiSettings by inject()
private val gestureSettings: GestureSettings by inject()
private val globalActionsService: GlobalActionsService by inject()
private val permissionsManager: PermissionsManager by inject()
private val searchableRepository: SavableSearchableRepository by inject()
@@ -40,40 +44,41 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
private var isSystemInDarkMode = MutableStateFlow(false)
private val dimBackgroundState = combine(
dataStore.data.map { it.appearance.dimWallpaper },
dataStore.data.map { it.appearance.theme },
uiSettings.dimWallpaper,
uiSettings.colorScheme,
isSystemInDarkMode
) { dim, theme, systemDarkMode ->
dim && (theme == Settings.AppearanceSettings.Theme.Dark || theme == Settings.AppearanceSettings.Theme.System && systemDarkMode)
dim && (theme == ColorScheme.Dark || theme == ColorScheme.System && systemDarkMode)
}
val dimBackground = dimBackgroundState.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
val statusBarColor = dataStore.data.map { it.systemBars.statusBarColor }
val statusBarColor = uiSettings.statusBarColor
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val navBarColor = dataStore.data.map { it.systemBars.statusBarColor }
val navBarColor = uiSettings.navigationBarColor
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val chargingAnimation = dataStore.data.map { it.animations.charging }
val chargingAnimation = uiSettings.chargingAnimation
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val hideNavBar = dataStore.data.map { it.systemBars.hideNavBar }
val hideNavBar = uiSettings.hideNavigationBar
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
val hideStatusBar = dataStore.data.map { it.systemBars.hideStatusBar }
val hideStatusBar = uiSettings.hideStatusBar
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
fun setSystemInDarkMode(darkMode: Boolean) {
isSystemInDarkMode.value = darkMode
}
val baseLayout = dataStore.data.map { it.layout.baseLayout }
val baseLayout = uiSettings.baseLayout
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
val bottomSearchBar = dataStore.data.map { it.layout.bottomSearchBar }
val bottomSearchBar = uiSettings.bottomSearchBar
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
val reverseSearchResults = dataStore.data.map { it.layout.reverseSearchResults }
val reverseSearchResults = uiSettings.reverseSearchResults
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
val fixedSearchBar = dataStore.data.map { it.layout.fixedSearchBar }
val fixedSearchBar = uiSettings.fixedSearchBar
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
val fixedRotation = dataStore.data.map { it.layout.fixedRotation }
val fixedRotation = uiSettings.orientation
.map { it != ScreenOrientation.Auto }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
val isSearchOpen = mutableStateOf(false)
@@ -81,8 +86,7 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
val searchBarFocused = mutableStateOf(false)
val autoFocusSearch = dataStore.data.map { it.searchBar.autoFocus }
val autoFocusSearch = uiSettings.openKeyboardOnSearch
fun setSearchbarFocus(focused: Boolean) {
if (searchBarFocused.value != focused) searchBarFocused.value = focused
@@ -120,45 +124,43 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
isWidgetEditMode.value = editMode
}
val wallpaperBlur = dataStore.data.map { it.appearance.blurWallpaper }
val wallpaperBlur = uiSettings.blurWallpaper
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), true)
val wallpaperBlurRadius = dataStore.data.map { it.appearance.blurWallpaperRadius }
val wallpaperBlurRadius = uiSettings.wallpaperBlurRadius
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 32)
val fillClockHeight = dataStore.data.map { it.clockWidget.fillHeight }
val fillClockHeight = uiSettings.clockFillScreen
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), true)
val searchBarColor = dataStore.data.map { it.searchBar.color }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Settings.SearchBarSettings.SearchBarColors.Auto)
val searchBarStyle = dataStore.data.map { it.searchBar.searchBarStyle }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Settings.SearchBarSettings.SearchBarStyle.Transparent)
val searchBarColor = uiSettings.searchBarColor
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), SearchBarColors.Auto)
val searchBarStyle = uiSettings.searchBarStyle
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), SearchBarStyle.Transparent)
val gestureState: StateFlow<GestureState> = dataStore
.data.map { it.gestures }
.distinctUntilChanged()
val gestureState: StateFlow<GestureState> = gestureSettings
.combine(baseLayout) { settings, layout ->
val swipeLeftAction =
settings?.swipeLeft?.takeIf { layout != Layout.Pager } ?: GestureAction.None
val swipeRightAction = settings?.swipeRight?.takeIf { layout != Layout.PagerReversed }
?: GestureAction.None
settings.swipeLeft.takeIf { layout != BaseLayout.Pager } ?: GestureAction.NoAction
val swipeRightAction = settings.swipeRight.takeIf { layout != BaseLayout.PagerReversed }
?: GestureAction.NoAction
val swipeDownAction =
settings?.swipeDown?.takeIf { layout != Layout.PullDown } ?: GestureAction.None
val longPressAction = settings?.longPress ?: GestureAction.None
val doubleTapAction = settings?.doubleTap ?: GestureAction.None
val homeButtonAction = settings?.homeButton ?: GestureAction.None
settings.swipeDown.takeIf { layout != BaseLayout.PullDown } ?: GestureAction.NoAction
val longPressAction = settings.longPress
val doubleTapAction = settings.doubleTap
val homeButtonAction = settings.homeButton
val swipeLeftAppKey =
if (swipeLeftAction == GestureAction.LaunchApp) settings.swipeLeftApp else null
if (swipeLeftAction is GestureAction.Launch) swipeLeftAction.key else null
val swipeRightAppKey =
if (swipeRightAction == GestureAction.LaunchApp) settings.swipeRightApp else null
if (swipeRightAction is GestureAction.Launch) swipeRightAction.key else null
val swipeDownAppKey =
if (swipeDownAction == GestureAction.LaunchApp) settings.swipeDownApp else null
if (swipeDownAction is GestureAction.Launch) swipeDownAction.key else null
val longPressAppKey =
if (longPressAction == GestureAction.LaunchApp) settings.longPressApp else null
if (longPressAction is GestureAction.Launch) longPressAction.key else null
val doubleTapAppKey =
if (doubleTapAction == GestureAction.LaunchApp) settings.doubleTapApp else null
if (doubleTapAction is GestureAction.Launch) doubleTapAction.key else null
val homeButtonAppKey =
if (homeButtonAction == GestureAction.LaunchApp) settings.homeButtonApp else null
if (homeButtonAction is GestureAction.Launch) homeButtonAction.key else null
val apps = listOfNotNull(
swipeLeftAppKey,
swipeRightAppKey,
@@ -189,17 +191,17 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
val action = when (gesture) {
Gesture.DoubleTap -> gestureState.value.doubleTapAction
Gesture.LongPress -> gestureState.value.longPressAction
Gesture.SwipeDown -> gestureState.value.swipeDownAction.takeIf { baseLayout.value != Layout.PullDown }
Gesture.SwipeLeft -> gestureState.value.swipeLeftAction.takeIf { baseLayout.value != Layout.Pager }
Gesture.SwipeRight -> gestureState.value.swipeRightAction.takeIf { baseLayout.value != Layout.PagerReversed }
Gesture.SwipeDown -> gestureState.value.swipeDownAction.takeIf { baseLayout.value != BaseLayout.PullDown }
Gesture.SwipeLeft -> gestureState.value.swipeLeftAction.takeIf { baseLayout.value != BaseLayout.Pager }
Gesture.SwipeRight -> gestureState.value.swipeRightAction.takeIf { baseLayout.value != BaseLayout.PagerReversed }
Gesture.HomeButton -> gestureState.value.homeButtonAction
}
val requiresAccessibilityService =
action == GestureAction.OpenRecents
|| action == GestureAction.OpenPowerDialog
|| action == GestureAction.OpenQuickSettings
|| action == GestureAction.OpenNotificationDrawer
|| action == GestureAction.LockScreen
action is GestureAction.Recents
|| action is GestureAction.PowerMenu
|| action is GestureAction.QuickSettings
|| action is GestureAction.Notifications
|| action is GestureAction.ScreenLock
if (action != null && requiresAccessibilityService && !permissionsManager.checkPermissionOnce(
PermissionGroup.Accessibility
@@ -211,37 +213,37 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
return when (action) {
GestureAction.OpenSearch -> {
is GestureAction.Search -> {
openSearch()
true
}
GestureAction.OpenNotificationDrawer -> {
is GestureAction.Notifications -> {
globalActionsService.openNotificationDrawer()
true
}
GestureAction.OpenQuickSettings -> {
is GestureAction.QuickSettings -> {
globalActionsService.openQuickSettings()
true
}
GestureAction.LockScreen -> {
is GestureAction.ScreenLock -> {
globalActionsService.lockScreen()
true
}
GestureAction.OpenPowerDialog -> {
is GestureAction.PowerMenu -> {
globalActionsService.openPowerDialog()
true
}
GestureAction.OpenRecents -> {
is GestureAction.Recents -> {
globalActionsService.openRecents()
true
}
GestureAction.LaunchApp -> {
is GestureAction.Launch -> {
val view = (context as Activity).window.decorView
val options = ActivityOptionsCompat.makeScaleUpAnimation(
view,
@@ -271,12 +273,12 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
}
data class GestureState(
val swipeLeftAction: GestureAction = GestureAction.None,
val swipeRightAction: GestureAction = GestureAction.None,
val swipeDownAction: GestureAction = GestureAction.None,
val longPressAction: GestureAction = GestureAction.None,
val doubleTapAction: GestureAction = GestureAction.None,
val homeButtonAction: GestureAction = GestureAction.None,
val swipeLeftAction: GestureAction = GestureAction.NoAction,
val swipeRightAction: GestureAction = GestureAction.NoAction,
val swipeDownAction: GestureAction = GestureAction.NoAction,
val longPressAction: GestureAction = GestureAction.NoAction,
val doubleTapAction: GestureAction = GestureAction.NoAction,
val homeButtonAction: GestureAction = GestureAction.NoAction,
val swipeLeftApp: SavableSearchable? = null,
val swipeRightApp: SavableSearchable? = null,
val swipeDownApp: SavableSearchable? = null,
@@ -81,11 +81,12 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarColors
import de.mm20.launcher2.preferences.SearchBarColors
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.SearchBarLevel
@@ -398,7 +399,7 @@ fun PagerScaffold(
.fillMaxSize()
.nestedScroll(pagerNestedScrollConnection),
beyondBoundsPageCount = 1,
reverseLayout = reverse,
reverseLayout = reverse == (LocalLayoutDirection.current == LayoutDirection.Ltr),
state = pagerState,
userScrollEnabled = false,//!isWidgetEditMode,
flingBehavior = PagerDefaults.flingBehavior(
@@ -406,7 +407,6 @@ fun PagerScaffold(
lowVelocityAnimationSpec = spring(
stiffness = Spring.StiffnessMediumLow,
),
snapVelocityThreshold = 1000.dp,
pagerSnapDistance = remember {
object : PagerSnapDistance {
override fun calculateTargetPage(
@@ -68,7 +68,7 @@ import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.SearchBarColors
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.SearchBarLevel
@@ -605,7 +605,7 @@ fun PullDownScaffold(
showHiddenItemsButton = isSearchOpen,
value = { value },
onValueChange = { searchVM.search(it) },
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == Settings.SearchBarSettings.SearchBarColors.Auto || searchBarColor == Settings.SearchBarSettings.SearchBarColors.Dark,
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark,
style = searchBarStyle,
reverse = bottomSearchBar,
onKeyboardActionGo = if (launchOnEnter) {
@@ -34,8 +34,8 @@ import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.Settings.SystemBarsSettings.SystemBarColors
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
@@ -184,7 +184,7 @@ abstract class SharedLauncherActivity(
}
} else {
when (layout) {
Settings.LayoutSettings.Layout.PullDown -> {
BaseLayout.PullDown -> {
key(bottomSearchBar, reverseSearchResults) {
PullDownScaffold(
modifier = Modifier
@@ -205,8 +205,8 @@ abstract class SharedLauncherActivity(
}
}
Settings.LayoutSettings.Layout.Pager,
Settings.LayoutSettings.Layout.PagerReversed -> {
BaseLayout.Pager,
BaseLayout.PagerReversed -> {
key(bottomSearchBar, reverseSearchResults) {
PagerScaffold(
modifier = Modifier
@@ -220,7 +220,7 @@ abstract class SharedLauncherActivity(
},
darkStatusBarIcons = lightStatus,
darkNavBarIcons = lightNav,
reverse = layout == Settings.LayoutSettings.Layout.PagerReversed,
reverse = layout == BaseLayout.PagerReversed,
bottomSearchBar = bottomSearchBar,
reverseSearchResults = reverseSearchResults,
fixedSearchBar = fixedSearchBar,
@@ -19,7 +19,7 @@ import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.preferences.Settings.GestureSettings.GestureAction
import de.mm20.launcher2.preferences.GestureAction
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.component.FakeSplashScreen
import de.mm20.launcher2.ui.gestures.Gesture
@@ -52,7 +52,7 @@ fun LauncherGestureHandler(
val gestureState by viewModel.gestureState.collectAsState(GestureState())
val shouldDetectDoubleTapGesture = gestureState.doubleTapAction != GestureAction.None
val shouldDetectDoubleTapGesture = gestureState.doubleTapAction !is GestureAction.NoAction
LaunchedEffect(shouldDetectDoubleTapGesture) {
gestureDetector.shouldDetectDoubleTaps = shouldDetectDoubleTapGesture
@@ -5,25 +5,30 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.files.settings.FileSearchSettings
import de.mm20.launcher2.searchable.SavableSearchableRepository
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings.SearchResultOrderingSettings.Ordering
import de.mm20.launcher2.preferences.LegacySettings.SearchResultOrderingSettings.Ordering
import de.mm20.launcher2.preferences.SearchResultOrder
import de.mm20.launcher2.preferences.search.CalendarSearchSettings
import de.mm20.launcher2.preferences.search.ContactSearchSettings
import de.mm20.launcher2.preferences.search.FavoritesSettings
import de.mm20.launcher2.preferences.search.FileSearchSettings
import de.mm20.launcher2.preferences.search.ShortcutSearchSettings
import de.mm20.launcher2.preferences.ui.SearchUiSettings
import de.mm20.launcher2.search.AppProfile
import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.Article
import de.mm20.launcher2.search.CalendarEvent
import de.mm20.launcher2.search.Contact
import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchService
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.Article
import de.mm20.launcher2.search.CalendarEvent
import de.mm20.launcher2.search.Website
import de.mm20.launcher2.search.data.Calculator
import de.mm20.launcher2.search.data.UnitConverter
import de.mm20.launcher2.searchable.SavableSearchableRepository
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.services.favorites.FavoritesService
import kotlinx.coroutines.CancellationException
@@ -32,7 +37,6 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
@@ -47,10 +51,14 @@ class SearchVM : ViewModel(), KoinComponent {
private val favoritesService: FavoritesService by inject()
private val searchableRepository: SavableSearchableRepository by inject()
private val permissionsManager: PermissionsManager by inject()
private val dataStore: LauncherDataStore by inject()
private val fileSearchSettings: FileSearchSettings by inject()
val launchOnEnter = dataStore.data.map { it.searchBar.launchOnEnter }
private val fileSearchSettings: FileSearchSettings by inject()
private val contactSearchSettings: ContactSearchSettings by inject()
private val calendarSearchSettings: CalendarSearchSettings by inject()
private val shortcutSearchSettings: ShortcutSearchSettings by inject()
private val searchUiSettings: SearchUiSettings by inject()
val launchOnEnter = searchUiSettings.launchOnEnter
.stateIn(viewModelScope, SharingStarted.Eagerly, false)
private val searchService: SearchService by inject()
@@ -70,10 +78,10 @@ class SearchVM : ViewModel(), KoinComponent {
val unitConverterResults = mutableStateOf<List<UnitConverter>>(emptyList())
val searchActionResults = mutableStateOf<List<SearchAction>>(emptyList())
val hiddenResultsButton = dataStore.data.map { it.searchBar.hiddenItemsButton }
val hiddenResultsButton = searchUiSettings.hiddenItemsButton
val hiddenResults = mutableStateOf<List<SavableSearchable>>(emptyList())
val favoritesEnabled = dataStore.data.map { it.favorites.enabled }
val favoritesEnabled = searchUiSettings.favorites
val hideFavorites = mutableStateOf(false)
private val hiddenItemKeys = searchableRepository
@@ -117,16 +125,9 @@ class SearchVM : ViewModel(), KoinComponent {
}
hideFavorites.value = query.isNotEmpty()
searchJob = viewModelScope.launch {
dataStore.data.collectLatest { settings ->
searchUiSettings.resultOrder.collectLatest { resultOrder ->
searchService.search(
query,
calculator = settings.calculatorSearch,
unitConverter = settings.unitConverterSearch,
calendars = settings.calendarSearch,
contacts = settings.contactsSearch,
shortcuts = settings.appShortcutSearch,
websites = settings.websiteSearch,
wikipedia = settings.wikipediaSearch,
query
).collectLatest { results ->
var resultsList = withContext(Dispatchers.Default) {
listOfNotNull(
@@ -151,13 +152,13 @@ class SearchVM : ViewModel(), KoinComponent {
emptyList()
} else {
val keys = resultsList.mapNotNull { (it as? SavableSearchable)?.key }
when (settings.resultOrdering.ordering) {
when (resultOrder) {
Ordering.LaunchCount -> searchableRepository.sortByRelevance(
SearchResultOrder.LaunchCount -> searchableRepository.sortByRelevance(
keys
).first()
Ordering.Weighted -> searchableRepository.sortByWeight(
SearchResultOrder.Weighted -> searchableRepository.sortByWeight(
keys
).first()
@@ -256,7 +257,7 @@ class SearchVM : ViewModel(), KoinComponent {
val missingCalendarPermission = combine(
permissionsManager.hasPermission(PermissionGroup.Calendar),
dataStore.data.map { it.calendarSearch.enabled }.distinctUntilChanged()
calendarSearchSettings.enabled,
) { perm, enabled -> !perm && enabled }
fun requestCalendarPermission(context: AppCompatActivity) {
@@ -264,18 +265,12 @@ class SearchVM : ViewModel(), KoinComponent {
}
fun disableCalendarSearch() {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setCalendarSearch(it.calendarSearch.toBuilder().setEnabled(false))
.build()
}
}
calendarSearchSettings.setEnabled(false)
}
val missingContactsPermission = combine(
permissionsManager.hasPermission(PermissionGroup.Contacts),
dataStore.data.map { it.contactsSearch.enabled }.distinctUntilChanged()
contactSearchSettings.enabled
) { perm, enabled -> !perm && enabled }
fun requestContactsPermission(context: AppCompatActivity) {
@@ -283,18 +278,12 @@ class SearchVM : ViewModel(), KoinComponent {
}
fun disableContactsSearch() {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setContactsSearch(it.contactsSearch.toBuilder().setEnabled(false))
.build()
}
}
contactSearchSettings.setEnabled(false)
}
val missingFilesPermission = combine(
permissionsManager.hasPermission(PermissionGroup.ExternalStorage),
fileSearchSettings.localFiles.distinctUntilChanged()
fileSearchSettings.localFiles
) { perm, enabled -> !perm && enabled }
fun requestFilesPermission(context: AppCompatActivity) {
@@ -302,18 +291,12 @@ class SearchVM : ViewModel(), KoinComponent {
}
fun disableFilesSearch() {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setFileSearch(it.fileSearch.toBuilder().setLocalFiles(false))
.build()
}
}
fileSearchSettings.setLocalFiles(false)
}
val missingAppShortcutPermission = combine(
permissionsManager.hasPermission(PermissionGroup.AppShortcuts),
dataStore.data.map { it.appShortcutSearch.enabled }.distinctUntilChanged()
shortcutSearchSettings.enabled,
) { perm, enabled -> !perm && enabled }
fun requestAppShortcutPermission(context: AppCompatActivity) {
@@ -321,12 +304,6 @@ class SearchVM : ViewModel(), KoinComponent {
}
fun disableAppShortcutSearch() {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setAppShortcutSearch(it.appShortcutSearch.toBuilder().setEnabled(false))
.build()
}
}
shortcutSearchSettings.setEnabled(false)
}
}
@@ -1,29 +1,22 @@
package de.mm20.launcher2.ui.launcher.search.favorites
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.ui.UiState
import de.mm20.launcher2.ui.common.FavoritesVM
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import org.koin.core.component.inject
class SearchFavoritesVM : FavoritesVM() {
override val tagsExpanded: Flow<Boolean> = dataStore.data.map { it.ui.searchTagsMultiline }
.shareIn(viewModelScope, SharingStarted.Lazily)
private val uiState: UiState by inject()
override val tagsExpanded: Flow<Boolean> = uiState.favoritesTagsExpanded
override fun setTagsExpanded(expanded: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setUi(
it.ui.toBuilder()
.setSearchTagsMultiline(expanded)
.build()
)
.build()
}
}
uiState.setFavoritesTagsExpanded(expanded)
}
}
@@ -19,7 +19,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalFocusManager
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.SearchBarStyle
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.ui.component.SearchBar
import de.mm20.launcher2.ui.component.SearchBarLevel
@@ -29,7 +29,7 @@ import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
@Composable
fun LauncherSearchBar(
modifier: Modifier = Modifier,
style: Settings.SearchBarSettings.SearchBarStyle,
style: SearchBarStyle,
level: () -> SearchBarLevel,
value: () -> String,
onValueChange: (String) -> Unit,
@@ -9,6 +9,7 @@ import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
import androidx.compose.animation.graphics.vector.AnimatedImageVector
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.HelpOutline
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.icons.rounded.Wallpaper
@@ -20,6 +21,7 @@ import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -27,7 +29,10 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.launcher.LauncherScaffoldVM
import de.mm20.launcher2.ui.launcher.widgets.WidgetsVM
import de.mm20.launcher2.ui.settings.SettingsActivity
@Composable
@@ -38,6 +43,8 @@ fun RowScope.SearchBarMenu(
val context = LocalContext.current
var showOverflowMenu by remember { mutableStateOf(false) }
val rightIcon = AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_menu_clear)
val launcherVM: LauncherScaffoldVM = viewModel()
val widgetsVM: WidgetsVM = viewModel()
IconButton(onClick = {
if (searchBarValue.isNotBlank()) onSearchBarValueChange("")
@@ -70,6 +77,22 @@ fun RowScope.SearchBarMenu(
Icon(imageVector = Icons.Rounded.Wallpaper, contentDescription = null)
}
)
val editButton by widgetsVM.editButton.collectAsState()
val searchOpen by launcherVM.isSearchOpen
if (!searchOpen && editButton == false) {
DropdownMenuItem(
onClick = {
launcherVM.setWidgetEditMode(editMode = true)
showOverflowMenu = false
},
text = {
Text(stringResource(R.string.menu_edit_widgets))
},
leadingIcon = {
Icon(imageVector = Icons.Rounded.Edit, contentDescription = null)
}
)
}
DropdownMenuItem(
onClick = {
context.startActivity(Intent(context, SettingsActivity::class.java))
@@ -1,8 +1,6 @@
package de.mm20.launcher2.ui.launcher.sheets
import android.app.Activity
import android.content.Context
import android.content.pm.LauncherApps
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.IntentSenderRequest
@@ -33,7 +31,6 @@ import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.icons.rounded.Tag
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Divider
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FilledTonalIconButton
@@ -65,12 +62,13 @@ import androidx.compose.ui.graphics.drawOutline
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toOffset
import androidx.compose.ui.unit.toSize
@@ -173,6 +171,7 @@ fun ReorderFavoritesGrid(viewModel: EditFavoritesSheetVM, paddingValues: Padding
val tagsListState = rememberLazyListState()
val tagsTitleSize = 48.dp.toPixels()
val tagsSpacing = 12.dp.toPixels()
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
val state = rememberLazyDragAndDropGridState(
gridState = gridState,
onDragStart = {
@@ -200,8 +199,11 @@ fun ReorderFavoritesGrid(viewModel: EditFavoritesSheetVM, paddingValues: Padding
&& hoveredItem.offset.y + tagsTitleSize < position.y
) {
val scroll = tagsListState.layoutInfo.viewportStartOffset
val relCenter =
if (isRtl) draggedCenter.copy(x = tagsListState.layoutInfo.viewportSize.width - draggedCenter.x)
else draggedCenter
val tag = tagsListState.layoutInfo.visibleItemsInfo.find {
draggedCenter.x + scroll > it.offset && draggedCenter.x + scroll < it.offset + it.size - tagsSpacing
relCenter.x + scroll > it.offset && relCenter.x + scroll < it.offset + it.size - tagsSpacing
}
hoveredTag = tag?.index?.let { pinnedTags[it].tag }
} else {
@@ -17,9 +17,9 @@ import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.normalize
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.appshortcuts.AppShortcut
import de.mm20.launcher2.preferences.search.FavoritesSettings
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.data.Tag
import de.mm20.launcher2.services.favorites.FavoritesService
@@ -41,7 +41,7 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
private val badgeService: BadgeService by inject()
private val customAttributesRepository: CustomAttributesRepository by inject()
private val permissionsManager: PermissionsManager by inject()
private val dataStore: LauncherDataStore by inject()
private val favoritesSettings: FavoritesSettings by inject()
val gridItems = mutableStateOf<List<FavoritesSheetGridItem>>(emptyList())
@@ -245,36 +245,16 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
}
}
val enableFrequentlyUsed = dataStore.data.map { it.favorites.frequentlyUsed }
val enableFrequentlyUsed = favoritesSettings.frequentlyUsed
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setFrequentlyUsed(frequentlyUsed: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setFavorites(
it.favorites
.toBuilder()
.setFrequentlyUsed(frequentlyUsed)
)
.build()
}
}
favoritesSettings.setFrequentlyUsed(frequentlyUsed)
}
val frequentlyUsedRows = dataStore.data.map { it.favorites.frequentlyUsedRows }
val frequentlyUsedRows = favoritesSettings.frequentlyUsedRows
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 0)
fun setFrequentlyUsedRows(frequentlyUsedRows: Int) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setFavorites(
it.favorites
.toBuilder()
.setFrequentlyUsedRows(frequentlyUsedRows)
)
.build()
}
}
favoritesSettings.setFrequentlyUsedRows(frequentlyUsedRows)
}
fun pinTag(tag: Tag) {
@@ -14,7 +14,8 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.GestureAction
import de.mm20.launcher2.preferences.LegacySettings
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.MissingPermissionBanner
@@ -29,12 +30,12 @@ fun FailedGestureSheet(
val viewModel: FailedGestureSheetVM = viewModel()
val actionName = stringResource(when(failedGesture.action) {
Settings.GestureSettings.GestureAction.OpenSearch -> R.string.gesture_action_open_search
Settings.GestureSettings.GestureAction.OpenNotificationDrawer -> R.string.gesture_action_notifications
Settings.GestureSettings.GestureAction.LockScreen -> R.string.gesture_action_lock_screen
Settings.GestureSettings.GestureAction.OpenQuickSettings -> R.string.gesture_action_quick_settings
Settings.GestureSettings.GestureAction.OpenRecents -> R.string.gesture_action_recents
Settings.GestureSettings.GestureAction.OpenPowerDialog -> R.string.gesture_action_power_menu
is GestureAction.Search -> R.string.gesture_action_open_search
is GestureAction.Notifications -> R.string.gesture_action_notifications
is GestureAction.ScreenLock -> R.string.gesture_action_lock_screen
is GestureAction.QuickSettings -> R.string.gesture_action_quick_settings
is GestureAction.Recents -> R.string.gesture_action_recents
is GestureAction.PowerMenu -> R.string.gesture_action_power_menu
else -> R.string.gesture_action_none
})
val gestureName = stringResource(when(failedGesture.gesture) {
@@ -5,8 +5,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings.GestureSettings.GestureAction
import de.mm20.launcher2.preferences.GestureAction
import de.mm20.launcher2.preferences.ui.GestureSettings
import de.mm20.launcher2.ui.gestures.Gesture
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
@@ -14,28 +14,20 @@ import org.koin.core.component.inject
class FailedGestureSheetVM : ViewModel(), KoinComponent {
private val permissionsManager: PermissionsManager by inject()
private val dataStore: LauncherDataStore by inject()
private val gestureSettings: GestureSettings by inject()
fun requestPermission(context: AppCompatActivity) {
permissionsManager.requestPermission(context, PermissionGroup.Accessibility)
}
fun disableGesture(gesture: Gesture) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder().setGestures(
it.gestures.toBuilder().apply {
when (gesture) {
Gesture.SwipeDown -> swipeDown = GestureAction.None
Gesture.SwipeLeft -> swipeLeft = GestureAction.None
Gesture.SwipeRight -> swipeRight = GestureAction.None
Gesture.DoubleTap -> doubleTap = GestureAction.None
Gesture.LongPress -> longPress = GestureAction.None
Gesture.HomeButton -> homeButton = GestureAction.None
}
}.build()
).build()
}
when(gesture) {
Gesture.DoubleTap -> gestureSettings.setDoubleTap(GestureAction.NoAction)
Gesture.LongPress -> gestureSettings.setLongPress(GestureAction.NoAction)
Gesture.SwipeDown -> gestureSettings.setSwipeDown(GestureAction.NoAction)
Gesture.SwipeLeft -> gestureSettings.setSwipeLeft(GestureAction.NoAction)
Gesture.SwipeRight -> gestureSettings.setSwipeRight(GestureAction.NoAction)
Gesture.HomeButton -> gestureSettings.setHomeButton(GestureAction.NoAction)
}
}
}
@@ -149,7 +149,7 @@ fun WidgetColumn(
}
val editButton by viewModel.editButton.collectAsState()
if (editButton == true) {
if (editMode || editButton == true) {
val icon =
AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_edit_add)
ExtendedFloatingActionButton(
@@ -1,9 +1,8 @@
package de.mm20.launcher2.ui.launcher.widgets
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.ui.UiSettings
import de.mm20.launcher2.widgets.Widget
import de.mm20.launcher2.widgets.WidgetRepository
import kotlinx.coroutines.flow.SharingStarted
@@ -15,9 +14,9 @@ import org.koin.core.component.inject
class WidgetsVM : ViewModel(), KoinComponent {
private val widgetRepository: WidgetRepository by inject()
private val dataStore: LauncherDataStore by inject()
private val uiSettings: UiSettings by inject()
val editButton = dataStore.data.map { it.widgets.editButton }
val editButton = uiSettings.widgetEditButton
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val widgets = widgetRepository.get()
@@ -14,8 +14,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@@ -64,10 +62,9 @@ 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.preferences.Settings.ClockWidgetSettings.ClockStyle
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetAlignment
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetColors
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout
import de.mm20.launcher2.preferences.ClockWidgetAlignment
import de.mm20.launcher2.preferences.ClockWidgetColors
import de.mm20.launcher2.preferences.ClockWidgetStyle
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.base.LocalTime
import de.mm20.launcher2.ui.component.BottomSheetDialog
@@ -89,7 +86,7 @@ fun ClockWidget(
) {
val viewModel: ClockWidgetVM = viewModel()
val context = LocalContext.current
val layout by viewModel.layout.collectAsState()
val compact by viewModel.compactLayout.collectAsState()
val clockStyle by viewModel.clockStyle.collectAsState()
val color by viewModel.color.collectAsState()
val alignment by viewModel.alignment.collectAsState()
@@ -106,7 +103,9 @@ fun ClockWidget(
viewModel.updateTime(time)
}
val partProvider by remember { viewModel.getActivePart(context) }.collectAsStateWithLifecycle(null)
val partProvider by remember { viewModel.getActivePart(context) }.collectAsStateWithLifecycle(
null
)
AnimatedContent(editMode, label = "ClockWidget") {
if (it) {
@@ -165,32 +164,32 @@ fun ClockWidget(
CompositionLocalProvider(
LocalContentColor provides contentColor
) {
if (layout == ClockWidgetLayout.Vertical) {
if (compact == false) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier.clickable(
enabled = clockStyle != ClockStyle.EmptyClock,
enabled = clockStyle !is ClockWidgetStyle.Empty,
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
viewModel.launchClockApp(context)
}
) {
Clock(clockStyle, ClockWidgetLayout.Vertical)
Clock(clockStyle, false)
}
if (partProvider != null) {
DynamicZone(
modifier = Modifier.padding(bottom = 8.dp),
layout = ClockWidgetLayout.Vertical,
compact = false,
provider = partProvider,
)
}
}
}
if (layout == ClockWidgetLayout.Horizontal) {
if (compact == true) {
Row(
modifier = Modifier
.fillMaxWidth()
@@ -201,7 +200,7 @@ fun ClockWidget(
if (partProvider != null) {
DynamicZone(
modifier = Modifier.weight(1f),
layout = ClockWidgetLayout.Horizontal,
compact = true,
provider = partProvider,
)
}
@@ -216,14 +215,14 @@ fun ClockWidget(
)
Box(
modifier = Modifier.clickable(
enabled = clockStyle != ClockStyle.EmptyClock,
enabled = clockStyle !is ClockWidgetStyle.Empty,
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
viewModel.launchClockApp(context)
}
) {
Clock(clockStyle, ClockWidgetLayout.Horizontal)
Clock(clockStyle, true)
}
}
}
@@ -236,7 +235,7 @@ fun ClockWidget(
.fillMaxWidth()
.padding(bottom = 16.dp)
) {
dockProvider?.Component(ClockWidgetLayout.Vertical)
dockProvider?.Component(false)
}
}
}
@@ -246,21 +245,18 @@ fun ClockWidget(
@Composable
fun Clock(
style: ClockStyle?,
layout: ClockWidgetLayout
style: ClockWidgetStyle?,
compact: Boolean,
) {
val time = LocalTime.current
when (style) {
ClockStyle.DigitalClock1,
ClockStyle.DigitalClock1_Outlined,
ClockStyle.DigitalClock1_MDY,
ClockStyle.DigitalClock1_OnePlus -> DigitalClock1(time, layout, style)
is ClockWidgetStyle.Digital1 -> DigitalClock1(time, compact, style)
ClockStyle.DigitalClock2 -> DigitalClock2(time, layout)
ClockStyle.BinaryClock -> BinaryClock(time, layout)
ClockStyle.AnalogClock -> AnalogClock(time, layout)
ClockStyle.OrbitClock -> OrbitClock(time, layout)
ClockStyle.EmptyClock -> {}
is ClockWidgetStyle.Digital2 -> DigitalClock2(time, compact)
is ClockWidgetStyle.Binary -> BinaryClock(time, compact)
is ClockWidgetStyle.Analog -> AnalogClock(time, compact)
is ClockWidgetStyle.Orbit -> OrbitClock(time, compact)
is ClockWidgetStyle.Empty -> {}
else -> {}
}
}
@@ -268,13 +264,13 @@ fun Clock(
@Composable
fun DynamicZone(
modifier: Modifier = Modifier,
layout: ClockWidgetLayout,
compact: Boolean,
provider: PartProvider?,
) {
Column(
modifier = modifier
) {
provider?.Component(layout)
provider?.Component(compact)
}
}
@@ -283,18 +279,13 @@ fun ConfigureClockWidgetSheet(
onDismiss: () -> Unit,
) {
val viewModel: ClockWidgetSettingsScreenVM = viewModel()
val layout by viewModel.layout.collectAsState()
val compact by viewModel.compact.collectAsState()
val color by viewModel.color.collectAsState()
val style by viewModel.clockStyle.collectAsState()
val fillHeight by viewModel.fillHeight.collectAsState()
val alignment by viewModel.alignment.collectAsState()
val date by viewModel.datePart.collectAsState()
val favorites by viewModel.favoritesPart.collectAsState()
val media by viewModel.musicPart.collectAsState()
val alarm by viewModel.alarmPart.collectAsState()
val battery by viewModel.batteryPart.collectAsState()
val dock by viewModel.dock.collectAsState()
val parts by viewModel.parts.collectAsState()
BottomSheetDialog(onDismissRequest = onDismiss) {
Column(
@@ -307,14 +298,14 @@ fun ConfigureClockWidgetSheet(
modifier = Modifier.fillMaxWidth(),
) {
SegmentedButton(
selected = layout == ClockWidgetLayout.Vertical,
selected = compact == false,
onClick = {
viewModel.setLayout(ClockWidgetLayout.Vertical)
viewModel.setCompact(false)
},
shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2),
icon = {
SegmentedButtonDefaults.Icon(
active = layout == ClockWidgetLayout.Vertical,
active = compact == false,
) {
Icon(
imageVector = Icons.Rounded.HorizontalSplit,
@@ -327,14 +318,14 @@ fun ConfigureClockWidgetSheet(
Text(text = stringResource(R.string.preference_clockwidget_layout_vertical))
}
SegmentedButton(
selected = layout == ClockWidgetLayout.Horizontal,
selected = compact == true,
onClick = {
viewModel.setLayout(ClockWidgetLayout.Horizontal)
viewModel.setCompact(true)
},
shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2),
icon = {
SegmentedButtonDefaults.Icon(
active = layout == ClockWidgetLayout.Horizontal,
active = compact == true,
) {
Icon(
imageVector = Icons.Rounded.VerticalSplit,
@@ -348,9 +339,9 @@ fun ConfigureClockWidgetSheet(
}
}
if (color != null && layout != null) {
if (color != null && compact != null) {
WatchFaceSelector(
layout = layout!!,
compact = compact!!,
colors = color!!,
selected = style,
onSelect = {
@@ -487,7 +478,7 @@ fun ConfigureClockWidgetSheet(
title = stringResource(R.string.preference_clockwidget_favorites_part),
summary = stringResource(R.string.preference_clockwidget_favorites_part_summary),
icon = Icons.Rounded.Star,
value = favorites == true,
value = dock == true,
onValueChanged = {
viewModel.setFavoritesPart(it)
}
@@ -510,7 +501,7 @@ fun ConfigureClockWidgetSheet(
title = stringResource(R.string.preference_clockwidget_date_part),
summary = stringResource(R.string.preference_clockwidget_date_part_summary),
icon = Icons.Rounded.Today,
value = date == true,
value = parts?.date == true,
onValueChanged = {
viewModel.setDatePart(it)
}
@@ -519,7 +510,7 @@ fun ConfigureClockWidgetSheet(
title = stringResource(R.string.preference_clockwidget_music_part),
summary = stringResource(R.string.preference_clockwidget_music_part_summary),
icon = Icons.Rounded.MusicNote,
value = media == true,
value = parts?.music == true,
onValueChanged = {
viewModel.setMusicPart(it)
}
@@ -528,7 +519,7 @@ fun ConfigureClockWidgetSheet(
title = stringResource(R.string.preference_clockwidget_alarm_part),
summary = stringResource(R.string.preference_clockwidget_alarm_part_summary),
icon = Icons.Rounded.Alarm,
value = alarm == true,
value = parts?.alarm == true,
onValueChanged = {
viewModel.setAlarmPart(it)
}
@@ -537,7 +528,7 @@ fun ConfigureClockWidgetSheet(
title = stringResource(R.string.preference_clockwidget_battery_part),
summary = stringResource(R.string.preference_clockwidget_battery_part_summary),
icon = Icons.Rounded.BatteryFull,
value = battery == true,
value = parts?.battery == true,
onValueChanged = {
viewModel.setBatteryPart(it)
}
@@ -6,7 +6,7 @@ import android.provider.AlarmClock
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.ui.ClockWidgetSettings
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
@@ -18,21 +18,20 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class ClockWidgetVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val settings: ClockWidgetSettings by inject()
private val partProviders = dataStore.data.map { it.clockWidget }.distinctUntilChanged().map {
private val partProviders = settings.parts.map {
val providers = mutableListOf<PartProvider>()
if (it.datePart) providers += DatePartProvider()
if (it.musicPart) providers += MusicPartProvider()
if (it.batteryPart) providers += BatteryPartProvider()
if (it.alarmPart) providers += AlarmPartProvider()
if (it.date) providers += DatePartProvider()
if (it.music) providers += MusicPartProvider()
if (it.battery) providers += BatteryPartProvider()
if (it.alarm) providers += AlarmPartProvider()
providers
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
@@ -51,19 +50,19 @@ class ClockWidgetVM : ViewModel(), KoinComponent {
}
}
val layout = dataStore.data.map { it.clockWidget.layout }
val compactLayout = settings.compact
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val clockStyle = dataStore.data.map { it.clockWidget.clockStyle }
val clockStyle = settings.clockStyle
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val color = dataStore.data.map { it.clockWidget.color }
val color = settings.color
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val alignment = dataStore.data.map { it.clockWidget.alignment }
val alignment = settings.alignment
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val dockProvider = dataStore.data
.map { if (it.clockWidget.favoritesPart) FavoritesPartProvider() else null }
val dockProvider = settings.dock
.map { if (it) FavoritesPartProvider() else null }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun updateTime(time: Long) {
@@ -43,19 +43,18 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockStyle
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetColors
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout
import de.mm20.launcher2.preferences.ClockWidgetColors
import de.mm20.launcher2.preferences.ClockWidgetStyle
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
import kotlinx.coroutines.launch
@Composable
fun WatchFaceSelector(
layout: ClockWidgetLayout,
compact: Boolean,
colors: ClockWidgetColors,
selected: ClockStyle?,
onSelect: (ClockStyle) -> Unit,
selected: ClockWidgetStyle?,
onSelect: (ClockWidgetStyle) -> Unit,
) {
val context = LocalContext.current
Surface(
@@ -73,7 +72,7 @@ fun WatchFaceSelector(
modifier = Modifier,
) {
val styles = remember {
sortedMapOf(
mapOf(
ClockStyle.DigitalClock1 to 0,
ClockStyle.DigitalClock1_Outlined to 0,
ClockStyle.DigitalClock1_MDY to 0,
@@ -159,9 +158,9 @@ fun WatchFaceSelector(
styles.filter { it.value == pageIndex }
}
if (currentPageStyles.containsKey(selected)) {
Clock(selected, layout)
Clock(selected, compact)
} else {
Clock(currentPageStyles.keys.first(), layout)
Clock(currentPageStyles.keys.first(), compact)
}
}
}
@@ -253,7 +252,7 @@ fun WatchFaceSelector(
}
}
fun getClockstyleName(context: Context, style: ClockStyle): String {
fun getClockstyleName(context: Context, style: ClockWidgetStyle): String {
return when (style) {
ClockStyle.DigitalClock1,
ClockStyle.DigitalClock1_Outlined,
@@ -264,11 +263,11 @@ fun getClockstyleName(context: Context, style: ClockStyle): String {
ClockStyle.BinaryClock -> "Binary"
ClockStyle.AnalogClock -> "Hands"
ClockStyle.EmptyClock -> "Empty"
ClockStyle.UNRECOGNIZED -> ""
else -> ""
}
}
fun getVariantName(context: Context, style: ClockStyle): String {
fun getVariantName(context: Context, style: ClockWidgetStyle): String {
return when (style) {
ClockStyle.DigitalClock1,
ClockStyle.DigitalClock2,
@@ -279,7 +278,20 @@ fun getVariantName(context: Context, style: ClockStyle): String {
ClockStyle.DigitalClock1_Outlined -> "Outlined"
ClockStyle.DigitalClock1_MDY -> "Material You"
ClockStyle.DigitalClock1_OnePlus -> "OnePlus"
else -> ""
ClockStyle.UNRECOGNIZED -> ""
}
}
// Compat for old enum names, TODO refactor this screen
object ClockStyle {
val DigitalClock1 = ClockWidgetStyle.Digital1()
val DigitalClock1_Outlined = ClockWidgetStyle.Digital1(outlined = true)
val DigitalClock1_MDY = ClockWidgetStyle.Digital1(variant = ClockWidgetStyle.Digital1.Variant.MDY)
val DigitalClock1_OnePlus = ClockWidgetStyle.Digital1(variant = ClockWidgetStyle.Digital1.Variant.OnePlus)
val DigitalClock2 = ClockWidgetStyle.Digital2
val OrbitClock = ClockWidgetStyle.Orbit
val AnalogClock = ClockWidgetStyle.Analog
val BinaryClock = ClockWidgetStyle.Binary
val EmptyClock = ClockWidgetStyle.Empty
}
@@ -11,15 +11,15 @@ import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.LegacySettings
import java.util.*
@Composable
fun AnalogClock(
time: Long,
layout: Settings.ClockWidgetSettings.ClockWidgetLayout
compact: Boolean,
) {
val verticalLayout = layout == Settings.ClockWidgetSettings.ClockWidgetLayout.Vertical
val verticalLayout = !compact
val date = Calendar.getInstance()
date.timeInMillis = time
val minute = date[Calendar.MINUTE]
@@ -7,15 +7,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.preferences.Settings
import java.util.*
@Composable
fun BinaryClock(
time: Long,
layout: Settings.ClockWidgetSettings.ClockWidgetLayout
compact: Boolean,
) {
val verticalLayout = layout == Settings.ClockWidgetSettings.ClockWidgetLayout.Vertical
val verticalLayout = !compact
val date = Calendar.getInstance()
date.timeInMillis = time
val minute = date[Calendar.MINUTE]
@@ -8,7 +8,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawStyle
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalContext
@@ -20,8 +19,7 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockStyle
import de.mm20.launcher2.preferences.ClockWidgetStyle
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import java.text.SimpleDateFormat
@@ -30,10 +28,10 @@ import java.util.*
@Composable
fun DigitalClock1(
time: Long,
layout: Settings.ClockWidgetSettings.ClockWidgetLayout,
variant: ClockStyle = ClockStyle.DigitalClock1,
compact: Boolean,
style: ClockWidgetStyle.Digital1 = ClockWidgetStyle.Digital1(),
) {
val verticalLayout = layout == Settings.ClockWidgetSettings.ClockWidgetLayout.Vertical
val verticalLayout = !compact
val format = SimpleDateFormat(
if (verticalLayout) {
if (DateFormat.is24HourFormat(LocalContext.current)) "HH\nmm" else "hh\nmm"
@@ -50,8 +48,8 @@ fun DigitalClock1(
fontWeight = FontWeight.Black,
textAlign = TextAlign.Center,
lineHeight = 0.8.em,
drawStyle = if (variant == ClockStyle.DigitalClock1_Outlined) Stroke(width = 2.dp.toPixels()) else Fill,
color = if (variant == ClockStyle.DigitalClock1_MDY) {
drawStyle = if (style.outlined) Stroke(width = 2.dp.toPixels()) else Fill,
color = if (style.variant == ClockWidgetStyle.Digital1.Variant.MDY) {
if (LocalContentColor.current == Color.White) {
if (LocalDarkTheme.current) MaterialTheme.colorScheme.onPrimaryContainer
else MaterialTheme.colorScheme.primaryContainer
@@ -64,7 +62,7 @@ fun DigitalClock1(
val modifier = Modifier.offset(0.dp, if (verticalLayout) 16.dp else 0.dp)
if (variant == ClockStyle.DigitalClock1_OnePlus) {
if (style.variant == ClockWidgetStyle.Digital1.Variant.OnePlus) {
val hour = formattedString.substring(0, 2)
Text(
modifier = modifier,
@@ -6,13 +6,12 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import de.mm20.launcher2.preferences.Settings
@Composable
fun DigitalClock2(
time: Long,
layout: Settings.ClockWidgetSettings.ClockWidgetLayout
compact: Boolean,
) {
Text(
text = DateUtils.formatDateTime(LocalContext.current, time, DateUtils.FORMAT_SHOW_TIME),
@@ -31,7 +31,7 @@ import androidx.compose.ui.unit.center
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toOffset
import de.mm20.launcher2.ktx.TWO_PI
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.LegacySettings
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
@@ -46,9 +46,9 @@ private val currentTime
@Composable
fun OrbitClock(
_time: Long,
layout: Settings.ClockWidgetSettings.ClockWidgetLayout
compact: Boolean,
) {
val verticalLayout = layout == Settings.ClockWidgetSettings.ClockWidgetLayout.Vertical
val verticalLayout = !compact
val timeState = remember { mutableStateOf<ZonedDateTime>(currentTime) }
@@ -21,7 +21,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.content.getSystemService
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.*
@@ -71,7 +70,7 @@ class AlarmPartProvider : PartProvider {
}
@Composable
override fun Component(layout: ClockWidgetLayout) {
override fun Component(compactLayout: Boolean) {
val context = LocalContext.current
val alarmTime by nextAlarmTime
@@ -79,7 +78,7 @@ class AlarmPartProvider : PartProvider {
alarmTime?.let {
if (layout == ClockWidgetLayout.Vertical) {
if (!compactLayout) {
TextButton(
onClick = {
@@ -20,14 +20,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.getSystemService
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.icons.*
import kotlinx.coroutines.channels.awaitClose
@@ -54,13 +52,13 @@ class BatteryPartProvider : PartProvider {
}
@Composable
override fun Component(layout: Settings.ClockWidgetSettings.ClockWidgetLayout) {
override fun Component(compactLayout: Boolean) {
val batteryInfo by this.batteryInfo.collectAsState(null)
batteryInfo?.let {
if (layout == Settings.ClockWidgetSettings.ClockWidgetLayout.Vertical) {
if (!compactLayout) {
Row(
Modifier.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
@@ -86,7 +84,7 @@ class BatteryPartProvider : PartProvider {
}
}
}
if (layout == Settings.ClockWidgetSettings.ClockWidgetLayout.Horizontal) {
if (compactLayout) {
Row(
Modifier.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
@@ -12,7 +12,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.em
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.ui.base.LocalTime
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
@@ -25,9 +24,9 @@ class DatePartProvider : PartProvider {
}
@Composable
override fun Component(layout: Settings.ClockWidgetSettings.ClockWidgetLayout) {
override fun Component(compactLayout: Boolean) {
val time = LocalTime.current
val verticalLayout = layout == Settings.ClockWidgetSettings.ClockWidgetLayout.Vertical
val verticalLayout = !compactLayout
val context = LocalContext.current
TextButton(
colors = ButtonDefaults.textButtonColors(
@@ -3,16 +3,13 @@ package de.mm20.launcher2.ui.launcher.widgets.clock.parts
import android.content.Context
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout
import de.mm20.launcher2.preferences.ui.UiSettings
import de.mm20.launcher2.services.favorites.FavoritesService
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
import de.mm20.launcher2.widgets.CalendarWidget
@@ -27,25 +24,24 @@ class FavoritesPartProvider : PartProvider, KoinComponent {
private val favoritesService: FavoritesService by inject()
private val widgetRepository: WidgetRepository by inject()
private val dataStore: LauncherDataStore by inject()
private val uiSettings: UiSettings by inject()
override fun getRanking(context: Context): Flow<Int> = flow {
emit(Int.MAX_VALUE)
}
@Composable
override fun Component(layout: ClockWidgetLayout) {
val columns by remember(layout) {
dataStore.data.map {
val c = it.grid.columnCount
if (layout == ClockWidgetLayout.Horizontal) c - 2 else c
override fun Component(compactLayout: Boolean) {
val columns by remember {
uiSettings.gridSettings.map {
it.columnCount
}
}.collectAsState(0)
val excludeCalendar by remember { widgetRepository.exists(CalendarWidget.Type) }.collectAsState(
true
)
val favorites by remember(columns, excludeCalendar, layout) {
val favorites by remember(columns, excludeCalendar) {
favoritesService.getFavorites(
excludeTypes = if (excludeCalendar) listOf("calendar", "tag") else listOf("tag"),
manuallySorted = true,
@@ -28,7 +28,6 @@ import androidx.compose.ui.unit.dp
import de.mm20.launcher2.music.MusicService
import de.mm20.launcher2.music.PlaybackState
import de.mm20.launcher2.music.SupportedActions
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout
import de.mm20.launcher2.ui.R
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
@@ -48,7 +47,7 @@ class MusicPartProvider : PartProvider, KoinComponent {
}
@Composable
override fun Component(layout: ClockWidgetLayout) {
override fun Component(compactLayout: Boolean) {
val context = LocalContext.current
val title by musicService.title.collectAsState(null)
@@ -58,7 +57,7 @@ class MusicPartProvider : PartProvider, KoinComponent {
val playIcon = AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_play_pause)
if (layout === ClockWidgetLayout.Horizontal) {
if (compactLayout) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
@@ -113,7 +112,7 @@ class MusicPartProvider : PartProvider, KoinComponent {
}
}
}
if (layout === ClockWidgetLayout.Vertical) {
if (!compactLayout) {
Column(
modifier = Modifier.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
@@ -2,7 +2,6 @@ package de.mm20.launcher2.ui.launcher.widgets.clock.parts
import android.content.Context
import androidx.compose.runtime.Composable
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout
import kotlinx.coroutines.flow.Flow
interface PartProvider {
@@ -12,5 +11,5 @@ interface PartProvider {
fun setTime(time: Long) {}
@Composable
fun Component(layout: ClockWidgetLayout)
fun Component(compactLayout: Boolean)
}
@@ -7,6 +7,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.Tag
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
@@ -30,6 +31,10 @@ fun FavoritesWidget(widget: FavoritesWidget) {
val tagsExpanded by viewModel.tagsExpanded.collectAsState(false)
LaunchedEffect(widget) {
viewModel.updateWidget(widget)
}
Column {
if (favorites.isNotEmpty()) {
SearchResultGrid(favorites)
@@ -1,32 +1,34 @@
package de.mm20.launcher2.ui.launcher.widgets.favorites
import de.mm20.launcher2.preferences.ui.GridSettings
import de.mm20.launcher2.preferences.ui.UiSettings
import de.mm20.launcher2.services.widgets.WidgetsService
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout
import de.mm20.launcher2.ui.common.FavoritesVM
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import de.mm20.launcher2.widgets.FavoritesWidget
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import org.koin.core.component.inject
class FavoritesWidgetVM : FavoritesVM() {
private val widgetsService: WidgetsService by inject()
override val tagsExpanded: Flow<Boolean> = dataStore.data.map { it.ui.widgetTagsMultiline }
.shareIn(viewModelScope, SharingStarted.Lazily)
private val uiSettings: UiSettings by inject()
override val tagsExpanded: MutableStateFlow<Boolean> = MutableStateFlow(false)
private val widget = MutableStateFlow<FavoritesWidget?>(null)
private val isTopWidget = widgetsService.isFavoritesWidgetFirst()
private val clockWidgetFavSlots = dataStore.data.combine(isTopWidget) { data, isTop ->
if (!isTop || !data.clockWidget.favoritesPart) 0
else {
if (data.clockWidget.layout == ClockWidgetLayout.Horizontal) data.grid.columnCount - 2
else data.grid.columnCount
private val clockWidgetFavSlots =
combine(uiSettings.dock, isTopWidget, uiSettings.gridSettings) { (dock, isTop, grid) ->
dock as Boolean
isTop as Boolean
grid as GridSettings
if (!isTop || !dock) 0
else {
grid.columnCount
}
}
}
override val favorites = super.favorites.combine(clockWidgetFavSlots) { favs, slots ->
if (selectedTag.value == null) {
@@ -38,17 +40,17 @@ class FavoritesWidgetVM : FavoritesVM() {
}
override fun setTagsExpanded(expanded: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setUi(
it.ui.toBuilder()
.setWidgetTagsMultiline(expanded)
.build()
)
.build()
}
}
this.tagsExpanded.value = expanded
val widget = this.widget.value ?: return
widgetsService.updateWidget(
widget.copy(
config = widget.config.copy(tagsMultiline = expanded)
)
)
}
fun updateWidget(widget: FavoritesWidget) {
tagsExpanded.value = widget.config.tagsMultiline
}
}
@@ -5,11 +5,10 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.*
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.weather.WeatherSettings
import de.mm20.launcher2.weather.DailyForecast
import de.mm20.launcher2.weather.Forecast
import de.mm20.launcher2.weather.WeatherRepository
import de.mm20.launcher2.weather.settings.WeatherSettings
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
@@ -26,8 +25,6 @@ class WeatherWidgetVM : ViewModel(), KoinComponent {
private val permissionsManager: PermissionsManager by inject()
private val dataStore: LauncherDataStore by inject()
/**
* Index of the currently selected day in [dailyForecasts]
*/
@@ -111,7 +108,8 @@ class WeatherWidgetVM : ViewModel(), KoinComponent {
val autoLocation = weatherSettings.autoLocation
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val imperialUnits = dataStore.data.map { it.weather.imperialUnits }
val imperialUnits = weatherSettings.imperialUnits
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
fun selectDay(index: Int) {
selectedDayIndex = min(index, forecasts.lastIndex)
@@ -5,8 +5,8 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.geometry.Size
import androidx.navigation.NavController
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.Settings.GridSettings
import de.mm20.launcher2.preferences.ui.CardStyle
import de.mm20.launcher2.preferences.ui.GridSettings
import de.mm20.launcher2.ui.theme.WallpaperColors
val LocalWindowSize = compositionLocalOf { Size(0f, 0f) }
@@ -15,11 +15,11 @@ val LocalAppWidgetHost = compositionLocalOf<AppWidgetHost?>(defaultFactory = { n
val LocalNavController = compositionLocalOf<NavController?> { null }
val LocalCardStyle = compositionLocalOf<Settings.CardSettings> { Settings.CardSettings.getDefaultInstance() }
val LocalCardStyle = compositionLocalOf { CardStyle() }
val LocalFavoritesEnabled = compositionLocalOf { true }
val LocalGridSettings = compositionLocalOf { GridSettings.newBuilder().setColumnCount(5).setShowLabels(true).setIconSize(48).build() }
val LocalGridSettings = compositionLocalOf { GridSettings() }
val LocalSnackbarHostState = compositionLocalOf { SnackbarHostState() }
@@ -34,7 +34,6 @@ import de.mm20.launcher2.ui.settings.appearance.AppearanceSettingsScreen
import de.mm20.launcher2.ui.settings.backup.BackupSettingsScreen
import de.mm20.launcher2.ui.settings.buildinfo.BuildInfoSettingsScreen
import de.mm20.launcher2.ui.settings.cards.CardsSettingsScreen
import de.mm20.launcher2.ui.settings.clockwidget.ClockWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.colorscheme.ThemeSettingsScreen
import de.mm20.launcher2.ui.settings.colorscheme.ThemesSettingsScreen
import de.mm20.launcher2.ui.settings.crashreporter.CrashReportScreen
@@ -170,9 +169,6 @@ class SettingsActivity : BaseActivity() {
composable(ROUTE_MEDIA_INTEGRATION) {
MediaIntegrationSettingsScreen()
}
composable("settings/homescreen/clock") {
ClockWidgetSettingsScreen()
}
composable("settings/favorites") {
FavoritesSettingsScreen()
}
@@ -10,9 +10,8 @@ import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.preferences.Settings.AppearanceSettings
import de.mm20.launcher2.preferences.Settings.AppearanceSettings.ColorScheme
import de.mm20.launcher2.preferences.Settings.AppearanceSettings.Theme
import de.mm20.launcher2.preferences.ColorScheme
import de.mm20.launcher2.preferences.Font
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.ListPreference
import de.mm20.launcher2.ui.component.preferences.Preference
@@ -28,22 +27,22 @@ fun AppearanceSettingsScreen() {
val viewModel: AppearanceSettingsScreenVM = viewModel()
val context = LocalContext.current
val navController = LocalNavController.current
val themeName by viewModel.colorSchemeName.collectAsStateWithLifecycle(null)
val themeName by viewModel.themeName.collectAsStateWithLifecycle(null)
PreferenceScreen(title = stringResource(id = R.string.preference_screen_appearance)) {
item {
PreferenceCategory {
val theme by viewModel.theme.collectAsState()
val theme by viewModel.colorScheme.collectAsState()
ListPreference(
title = stringResource(id = R.string.preference_theme),
items = listOf(
stringResource(id = R.string.preference_theme_system) to Theme.System,
stringResource(id = R.string.preference_theme_light) to Theme.Light,
stringResource(id = R.string.preference_theme_dark) to Theme.Dark,
stringResource(id = R.string.preference_theme_system) to ColorScheme.System,
stringResource(id = R.string.preference_theme_light) to ColorScheme.Light,
stringResource(id = R.string.preference_theme_dark) to ColorScheme.Dark,
),
value = theme,
onValueChanged = { newValue ->
if (newValue == null) return@ListPreference
viewModel.setTheme(newValue)
viewModel.setColorScheme(newValue)
}
)
Preference(
@@ -54,13 +53,13 @@ fun AppearanceSettingsScreen() {
}
)
if (isAtLeastApiLevel(31)) {
val compatMode by viewModel.compatMode.collectAsState()
val compatModeColors by viewModel.compatModeColors.collectAsState()
SwitchPreference(
title = stringResource(id = R.string.preference_force_compat_system_colors),
summary = stringResource(id = R.string.preference_force_compat_system_colors_summary),
value = compatMode,
value = compatModeColors,
onValueChanged = {
viewModel.setCompatMode(it)
viewModel.setCompatModeColors(it)
}
)
}
@@ -68,8 +67,8 @@ fun AppearanceSettingsScreen() {
ListPreference(
title = stringResource(R.string.preference_font),
items = listOf(
"Outfit" to AppearanceSettings.Font.Outfit,
stringResource(R.string.preference_font_system) to AppearanceSettings.Font.SystemDefault,
"Outfit" to Font.Outfit,
stringResource(R.string.preference_font_system) to Font.System,
),
value = font,
onValueChanged = {
@@ -3,74 +3,47 @@ package de.mm20.launcher2.ui.settings.appearance
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.icons.IconService
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings.AppearanceSettings.Font
import de.mm20.launcher2.preferences.Settings.AppearanceSettings.Theme
import de.mm20.launcher2.preferences.ColorScheme
import de.mm20.launcher2.preferences.Font
import de.mm20.launcher2.preferences.ui.UiSettings
import de.mm20.launcher2.themes.ThemeRepository
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.util.UUID
class AppearanceSettingsScreenVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val uiSettings: UiSettings by inject()
private val iconService: IconService by inject()
private val themeRepository: ThemeRepository by inject()
val theme = dataStore.data.map { it.appearance.theme }
val colorScheme = uiSettings.colorScheme
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setTheme(theme: Theme) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setAppearance(it.appearance.toBuilder().setTheme(theme))
.build()
}
}
fun setColorScheme(colorScheme: ColorScheme) {
uiSettings.setColorScheme(colorScheme)
}
val colorSchemeName = dataStore.data.map {
it.appearance.themeId?.takeIf { it.isNotEmpty() }?.let {
UUID.fromString(it)
}
}
.flatMapLatest {
val themeName = uiSettings.theme.flatMapLatest {
themeRepository.getThemeOrDefault(it)
}.map {
it.name
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val compatMode = dataStore.data.map {
it.appearance.forceCompatModeSystemColors
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
val compatModeColors = uiSettings.compatModeColors
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
fun setCompatMode(enabled: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setAppearance(it.appearance.toBuilder().setForceCompatModeSystemColors(enabled))
.build()
}
}
fun setCompatModeColors(enabled: Boolean) {
uiSettings.setCompatModeColors(enabled)
}
val font = dataStore.data.map { it.appearance.font }
val font = uiSettings.font
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setFont(font: Font) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setAppearance(it.appearance.toBuilder().setFont(font))
.build()
}
}
uiSettings.setFont(font)
}
}
@@ -3,7 +3,6 @@ package de.mm20.launcher2.ui.settings.buildinfo
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.accounts.AccountType
import de.mm20.launcher2.accounts.AccountsRepository
import de.mm20.launcher2.preferences.Settings.WeatherSettings.WeatherProvider
import de.mm20.launcher2.weather.WeatherRepository
import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent
@@ -15,7 +15,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.LegacySettings
import de.mm20.launcher2.preferences.SurfaceShape
import de.mm20.launcher2.preferences.ui.CardStyle
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.LauncherCard
import de.mm20.launcher2.ui.component.preferences.ListPreference
@@ -26,6 +28,9 @@ import de.mm20.launcher2.ui.component.preferences.SliderPreference
@Composable
fun CardsSettingsScreen() {
val viewModel: CardsSettingsScreenVM = viewModel()
val cardStyle by viewModel.cardStyle.collectAsState(CardStyle())
PreferenceScreen(title = stringResource(R.string.preference_cards)) {
item {
Box(
@@ -43,23 +48,21 @@ fun CardsSettingsScreen() {
}
item {
PreferenceCategory {
val shape by viewModel.shape.collectAsState()
ListPreference(
icon = Icons.Rounded.Rectangle,
title = stringResource(R.string.preference_cards_shape),
items = listOf(
stringResource(R.string.preference_cards_shape_rounded) to Settings.CardSettings.Shape.Rounded,
stringResource(R.string.preference_cards_shape_cut) to Settings.CardSettings.Shape.Cut,
stringResource(R.string.preference_cards_shape_rounded) to SurfaceShape.Rounded,
stringResource(R.string.preference_cards_shape_cut) to SurfaceShape.Cut,
),
value = shape,
value = cardStyle.shape,
onValueChanged = {
if (it != null) viewModel.setShape(it)
viewModel.setShape(it)
})
val radius by viewModel.radius.collectAsState()
SliderPreference(
title = stringResource(R.string.preference_cards_corner_radius),
icon = Icons.Rounded.RoundedCorner,
value = radius,
value = cardStyle.cornerRadius,
min = 0,
max = 24,
step = 1,
@@ -67,22 +70,20 @@ fun CardsSettingsScreen() {
viewModel.setRadius(it)
}
)
val opacity by viewModel.opacity.collectAsState()
SliderPreference(
title = stringResource(R.string.preference_cards_opacity),
icon = Icons.Rounded.Opacity,
value = opacity,
value = cardStyle.opacity,
min = 0f,
max = 1f,
onValueChanged = {
viewModel.setOpacity(it)
}
)
val borderWidth by viewModel.borderWidth.collectAsState()
SliderPreference(
title = stringResource(R.string.preference_cards_stroke_width),
icon = Icons.Rounded.LineWeight,
value = borderWidth,
value = cardStyle.borderWidth,
min = 0,
max = 8,
step = 1,
@@ -1,70 +1,29 @@
package de.mm20.launcher2.ui.settings.cards
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import de.mm20.launcher2.preferences.SurfaceShape
import de.mm20.launcher2.preferences.ui.UiSettings
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class CardsSettingsScreenVM: ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject()
class CardsSettingsScreenVM : ViewModel(), KoinComponent {
private val uiSettings: UiSettings by inject()
val cardStyle = uiSettings.cardStyle
val opacity = dataStore.data.map { it.cards.opacity }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 0f)
fun setOpacity(opacity: Float) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setCards(it.cards.toBuilder()
.setOpacity(opacity)
).build()
}
}
uiSettings.setCardOpacity(opacity)
}
val radius = dataStore.data.map { it.cards.radius }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 0)
fun setRadius(radius: Int) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setCards(it.cards.toBuilder()
.setRadius(radius)
).build()
}
}
uiSettings.setCardRadius(radius)
}
val borderWidth = dataStore.data.map { it.cards.borderWidth }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 0)
fun setBorderWidth(borderWidth: Int) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setCards(it.cards.toBuilder()
.setBorderWidth(borderWidth)
).build()
}
}
uiSettings.setCardBorderWidth(borderWidth)
}
val shape = dataStore.data.map { it.cards.shape }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setShape(shape: Settings.CardSettings.Shape) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setCards(it.cards.toBuilder()
.setShape(shape)
).build()
}
}
fun setShape(shape: SurfaceShape) {
uiSettings.setCardShape(shape)
}
}
@@ -1,204 +0,0 @@
package de.mm20.launcher2.ui.settings.clockwidget
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.*
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.pager.HorizontalPagerIndicator
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockStyle
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetColors
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout
import de.mm20.launcher2.ui.launcher.widgets.clock.Clock
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.*
@Composable
fun ClockWidgetSettingsScreen() {
val viewModel: ClockWidgetSettingsScreenVM = viewModel()
PreferenceScreen(
title = stringResource(R.string.preference_screen_clockwidget),
helpUrl = "https://kvaesitso.mm20.de/docs/user-guide/widgets/clock"
) {
item {
PreferenceCategory {
val layout by viewModel.layout.collectAsState()
ListPreference(
title = stringResource(R.string.preference_clockwidget_layout),
value = layout,
items = listOf(
stringResource(R.string.preference_clockwidget_layout_vertical) to ClockWidgetLayout.Vertical,
stringResource(R.string.preference_clockwidget_layout_horizontal) to ClockWidgetLayout.Horizontal,
),
onValueChanged = {
if (it != null) viewModel.setLayout(it)
}
)
val clockStyle by viewModel.clockStyle.collectAsState()
ClockStylePreference(
layout = layout ?: ClockWidgetLayout.Vertical,
value = clockStyle,
onValueChanged = {
viewModel.setClockStyle(it)
}
)
val color by viewModel.color.collectAsState()
ListPreference(
title = stringResource(R.string.preference_clock_widget_color),
value = color,
items = listOf(
stringResource(R.string.preference_system_bar_icons_auto) to ClockWidgetColors.Auto,
stringResource(R.string.preference_system_bar_icons_light) to ClockWidgetColors.Light,
stringResource(R.string.preference_system_bar_icons_dark) to ClockWidgetColors.Dark,
),
onValueChanged = {
if (it != null) viewModel.setColor(it)
}
)
val fillHeight by viewModel.fillHeight.collectAsState()
SwitchPreference(
title = stringResource(R.string.preference_clock_widget_fill_height),
summary = stringResource(R.string.preference_clock_widget_fill_height_summary),
value = fillHeight == true,
onValueChanged = { viewModel.setFillHeight(it) }
)
}
}
item {
PreferenceCategory {
val datePart by viewModel.datePart.collectAsState()
SwitchPreference(
title = stringResource(R.string.preference_clockwidget_date_part),
summary = stringResource(R.string.preference_clockwidget_date_part_summary),
icon = Icons.Rounded.Today,
value = datePart == true,
onValueChanged = {
viewModel.setDatePart(it)
},
)
val favoritesPart by viewModel.favoritesPart.collectAsState()
SwitchPreference(
title = stringResource(R.string.preference_clockwidget_favorites_part),
summary = stringResource(R.string.preference_clockwidget_favorites_part_summary),
icon = Icons.Rounded.Star,
value = favoritesPart == true,
onValueChanged = {
viewModel.setFavoritesPart(it)
},
)
}
}
item {
PreferenceCategory {
val musicPart by viewModel.musicPart.collectAsState()
SwitchPreference(
title = stringResource(R.string.preference_clockwidget_music_part),
summary = stringResource(R.string.preference_clockwidget_music_part_summary),
icon = Icons.Rounded.MusicNote,
value = musicPart == true,
onValueChanged = {
viewModel.setMusicPart(it)
}
)
val alarmPart by viewModel.alarmPart.collectAsState()
SwitchPreference(
title = stringResource(R.string.preference_clockwidget_alarm_part),
summary = stringResource(R.string.preference_clockwidget_alarm_part_summary),
icon = Icons.Rounded.Alarm,
value = alarmPart == true,
onValueChanged = {
viewModel.setAlarmPart(it)
}
)
val batteryPart by viewModel.batteryPart.collectAsState()
SwitchPreference(
title = stringResource(R.string.preference_clockwidget_battery_part),
summary = stringResource(R.string.preference_clockwidget_battery_part_summary),
icon = Icons.Rounded.BatteryFull,
value = batteryPart == true,
onValueChanged = {
viewModel.setBatteryPart(it)
}
)
}
}
}
}
@Composable
fun ClockStylePreference(
layout: ClockWidgetLayout,
value: ClockStyle?,
onValueChanged: (ClockStyle) -> Unit
) {
var showDialog by remember { mutableStateOf(false) }
Preference(
title = stringResource(R.string.preference_clock_widget_style),
summary = stringResource(R.string.preference_clock_widget_style_summary),
onClick = {
showDialog = true
}
)
if (showDialog && value != null) {
val styles = remember {
ClockStyle.values().filter { it != ClockStyle.UNRECOGNIZED }
}
val pagerState = rememberPagerState(styles.indexOf(value)) { styles.size }
AlertDialog(
onDismissRequest = { showDialog = false },
confirmButton = {
TextButton(onClick = {
showDialog = false
onValueChanged(styles[pagerState.currentPage])
}) {
Text(
text = stringResource(android.R.string.ok),
)
}
},
dismissButton = {
TextButton(onClick = { showDialog = false }) {
Text(
text = stringResource(android.R.string.cancel),
)
}
},
text = {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
HorizontalPager(
state = pagerState,
modifier = Modifier.height(300.dp).fillMaxWidth()
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Clock(
style = styles[it],
layout = layout
)
}
}
HorizontalPagerIndicator(pagerState = pagerState, pageCount = styles.size)
}
}
)
}
}
@@ -2,9 +2,10 @@ package de.mm20.launcher2.ui.settings.clockwidget
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetColors
import de.mm20.launcher2.preferences.ClockWidgetAlignment
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.map
import kotlinx.coroutines.flow.stateIn
@@ -13,146 +14,62 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class ClockWidgetSettingsScreenVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject()
val layout = dataStore.data.map { it.clockWidget.layout }
private val settings: ClockWidgetSettings by inject()
val compact = settings.compact
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setLayout(layout: ClockWidgetSettings.ClockWidgetLayout) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setClockWidget(
it.clockWidget.toBuilder()
.setLayout(layout)
).build()
}
}
fun setCompact(compact: Boolean) {
settings.setCompact(compact)
}
val clockStyle = dataStore.data.map { it.clockWidget.clockStyle }
val clockStyle = settings.clockStyle
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setClockStyle(clockStyle: ClockWidgetSettings.ClockStyle) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setClockWidget(
it.clockWidget.toBuilder()
.setClockStyle(clockStyle)
).build()
}
}
fun setClockStyle(clockStyle: ClockWidgetStyle) {
settings.setClockStyle(clockStyle)
}
val color = dataStore.data.map { it.clockWidget.color }
val color = settings.color
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setColor(color: ClockWidgetColors) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setClockWidget(
it.clockWidget.toBuilder()
.setColor(color)
).build()
}
}
settings.setColor(color)
}
val fillHeight = dataStore.data.map { it.clockWidget.fillHeight }
val fillHeight = settings.fillHeight
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setFillHeight(fillHeight: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setClockWidget(
it.clockWidget.toBuilder()
.setFillHeight(fillHeight)
).build()
}
}
settings.setFillHeight(fillHeight)
}
val datePart = dataStore.data.map { it.clockWidget.datePart }
val dock = settings.dock
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val parts = settings.parts
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setDatePart(datePart: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setClockWidget(
it.clockWidget.toBuilder()
.setDatePart(datePart)
).build()
}
}
settings.setDatePart(datePart)
}
val favoritesPart = dataStore.data.map { it.clockWidget.favoritesPart }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setFavoritesPart(favoritesPart: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setClockWidget(
it.clockWidget.toBuilder()
.setFavoritesPart(favoritesPart)
).build()
}
}
settings.setDock(favoritesPart)
}
val batteryPart = dataStore.data.map { it.clockWidget.batteryPart }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setBatteryPart(batteryPart: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setClockWidget(
it.clockWidget.toBuilder()
.setBatteryPart(batteryPart)
).build()
}
}
settings.setBatteryPart(batteryPart)
}
val musicPart = dataStore.data.map { it.clockWidget.musicPart }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setMusicPart(musicPart: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setClockWidget(
it.clockWidget.toBuilder()
.setMusicPart(musicPart)
).build()
}
}
settings.setMusicPart(musicPart)
}
val alarmPart = dataStore.data.map { it.clockWidget.alarmPart }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setAlarmPart(alarmPart: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setClockWidget(
it.clockWidget.toBuilder()
.setAlarmPart(alarmPart)
).build()
}
}
settings.setAlarmPart(alarmPart)
}
val alignment = dataStore.data.map { it.clockWidget.alignment }
val alignment = settings.alignment
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setAlignment(alignment: ClockWidgetSettings.ClockWidgetAlignment) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setClockWidget(
it.clockWidget.toBuilder()
.setAlignment(alignment)
).build()
}
}
fun setAlignment(alignment: ClockWidgetAlignment) {
settings.setAlignment(alignment)
}
}
@@ -2,24 +2,20 @@ package de.mm20.launcher2.ui.settings.colorscheme
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.protobuf.ByteString
import de.mm20.launcher2.ktx.toBytes
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.ThemeDescriptor
import de.mm20.launcher2.preferences.ui.UiSettings
import de.mm20.launcher2.themes.BlackAndWhiteThemeId
import de.mm20.launcher2.themes.DefaultThemeId
import de.mm20.launcher2.themes.Theme
import de.mm20.launcher2.themes.ThemeRepository
import de.mm20.launcher2.themes.fromJson
import de.mm20.launcher2.themes.toJson
import de.mm20.launcher2.ui.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -31,12 +27,14 @@ import java.util.UUID
class ThemesSettingsScreenVM : ViewModel(), KoinComponent {
private val themeRepository: ThemeRepository by inject()
private val dataStore: LauncherDataStore by inject()
private val uiSettings: UiSettings by inject()
val selectedTheme: Flow<UUID?> = dataStore.data.map {
it.appearance.themeId?.takeIf { it.isNotEmpty() }?.let {
UUID.fromString(it)
} ?: DefaultThemeId
val selectedTheme = uiSettings.theme.map {
when(it) {
ThemeDescriptor.Default -> DefaultThemeId
ThemeDescriptor.BlackAndWhite -> BlackAndWhiteThemeId
is ThemeDescriptor.Custom -> UUID.fromString(it.id)
}
}
val themes: Flow<List<Theme>> = themeRepository.getThemes()
@@ -49,15 +47,10 @@ class ThemesSettingsScreenVM : ViewModel(), KoinComponent {
}
fun selectTheme(theme: Theme) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setAppearance(
it.appearance.toBuilder()
.setThemeId(theme.id.toString())
)
.build()
}
when(theme.id) {
DefaultThemeId -> ThemeDescriptor.Default
BlackAndWhiteThemeId -> ThemeDescriptor.BlackAndWhite
else -> ThemeDescriptor.Custom(theme.id.toString())
}
}
@@ -2,24 +2,20 @@ package de.mm20.launcher2.ui.settings.easteregg
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.IconShape
import de.mm20.launcher2.preferences.ui.UiSettings
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class EasterEggSettingsScreenVM: ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val settings: UiSettings by inject()
val easterEgg = dataStore.data.map { it.easterEgg }
val easterEgg = settings.iconShape.map { it == IconShape.EasterEgg }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
fun setEasterEgg(easterEgg: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder().setEasterEgg(easterEgg).build()
}
}
settings.setIconShape(if (easterEgg) IconShape.EasterEgg else IconShape.PlatformDefault)
}
}
@@ -14,7 +14,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.preferences.Settings.SearchResultOrderingSettings.WeightFactor
import de.mm20.launcher2.preferences.WeightFactor
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.ListPreference
import de.mm20.launcher2.ui.component.preferences.Preference
@@ -2,8 +2,9 @@ package de.mm20.launcher2.ui.settings.favorites
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings.SearchResultOrderingSettings.WeightFactor
import de.mm20.launcher2.preferences.WeightFactor
import de.mm20.launcher2.preferences.search.FavoritesSettings
import de.mm20.launcher2.preferences.search.RankingSettings
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
@@ -12,65 +13,30 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class FavoritesSettingsScreenVM: ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val favoritesSettings: FavoritesSettings by inject()
private val rankingSettings: RankingSettings by inject()
val frequentlyUsed = dataStore.data.map { it.favorites.frequentlyUsed }
val frequentlyUsed = favoritesSettings.frequentlyUsed
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setFrequentlyUsed(frequentlyUsed: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setFavorites(
it.favorites.toBuilder()
.setFrequentlyUsed(frequentlyUsed)
)
.build()
}
}
favoritesSettings.setFrequentlyUsed(frequentlyUsed)
}
val frequentlyUsedRows = dataStore.data.map { it.favorites.frequentlyUsedRows }
val frequentlyUsedRows = favoritesSettings.frequentlyUsedRows
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
fun setFrequentlyUsedRows(frequentlyUsedRows: Int) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setFavorites(
it.favorites.toBuilder()
.setFrequentlyUsedRows(frequentlyUsedRows)
)
.build()
}
}
favoritesSettings.setFrequentlyUsedRows(frequentlyUsedRows)
}
val editButton = dataStore.data.map { it.favorites.editButton }
val editButton = favoritesSettings.showEditButton
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setEditButton(editButton: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setFavorites(
it.favorites.toBuilder()
.setEditButton(editButton)
)
.build()
}
}
favoritesSettings.setShowEditButton(editButton)
}
val searchResultWeightFactor = dataStore.data.map { it.resultOrdering.weightFactor }
val searchResultWeightFactor = rankingSettings.weightFactor
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), WeightFactor.Default)
fun setSearchResultWeightFactor(searchResultWeightFactor: WeightFactor) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setResultOrdering(
it.resultOrdering.toBuilder()
.setWeightFactor(searchResultWeightFactor)
)
.build()
}
}
rankingSettings.setWeightFactor(searchResultWeightFactor)
}
}
@@ -7,11 +7,11 @@ import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.accounts.Account
import de.mm20.launcher2.accounts.AccountType
import de.mm20.launcher2.accounts.AccountsRepository
import de.mm20.launcher2.files.settings.FileSearchSettings
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.plugins.PluginService
import de.mm20.launcher2.preferences.search.FileSearchSettings
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@@ -27,8 +27,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.preferences.Settings.GestureSettings.GestureAction
import de.mm20.launcher2.preferences.Settings.LayoutSettings.Layout
import de.mm20.launcher2.preferences.BaseLayout
import de.mm20.launcher2.preferences.GestureAction
import de.mm20.launcher2.preferences.LegacySettings.LayoutSettings.Layout
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.common.SearchablePicker
@@ -47,14 +48,14 @@ fun GestureSettingsScreen() {
val hasPermission by viewModel.hasPermission.collectAsStateWithLifecycle(null)
val options = buildList {
add(stringResource(R.string.gesture_action_none) to GestureAction.None)
add(stringResource(R.string.gesture_action_notifications) to GestureAction.OpenNotificationDrawer)
add(stringResource(R.string.gesture_action_quick_settings) to GestureAction.OpenQuickSettings)
if (isAtLeastApiLevel(28)) add(stringResource(R.string.gesture_action_lock_screen) to GestureAction.LockScreen)
add(stringResource(R.string.gesture_action_recents) to GestureAction.OpenRecents)
add(stringResource(R.string.gesture_action_power_menu) to GestureAction.OpenPowerDialog)
add(stringResource(R.string.gesture_action_open_search) to GestureAction.OpenSearch)
add(stringResource(R.string.gesture_action_launch_app) to GestureAction.LaunchApp)
add(stringResource(R.string.gesture_action_none) to GestureAction.NoAction)
add(stringResource(R.string.gesture_action_notifications) to GestureAction.Notifications)
add(stringResource(R.string.gesture_action_quick_settings) to GestureAction.QuickSettings)
if (isAtLeastApiLevel(28)) add(stringResource(R.string.gesture_action_lock_screen) to GestureAction.ScreenLock)
add(stringResource(R.string.gesture_action_recents) to GestureAction.Recents)
add(stringResource(R.string.gesture_action_power_menu) to GestureAction.PowerMenu)
add(stringResource(R.string.gesture_action_open_search) to GestureAction.Search)
add(stringResource(R.string.gesture_action_launch_app) to GestureAction.Launch(null))
}
val context = LocalContext.current
@@ -64,9 +65,9 @@ fun GestureSettingsScreen() {
val baseLayout by viewModel.baseLayout.collectAsStateWithLifecycle(null)
ListPreference(title = stringResource(R.string.preference_layout_open_search),
items = listOf(
stringResource(R.string.open_search_pull_down) to Layout.PullDown,
stringResource(R.string.open_search_swipe_left) to Layout.Pager,
stringResource(R.string.open_search_swipe_right) to Layout.PagerReversed,
stringResource(R.string.open_search_pull_down) to BaseLayout.PullDown,
stringResource(R.string.open_search_swipe_left) to BaseLayout.Pager,
stringResource(R.string.open_search_swipe_right) to BaseLayout.PagerReversed,
),
value = baseLayout,
onValueChanged = {
@@ -125,7 +126,7 @@ fun GestureSettingsScreen() {
)
val swipeDown by viewModel.swipeDown.collectAsStateWithLifecycle(null)
val swipeDownIsSearch = layout == Layout.PullDown
val swipeDownIsSearch = layout == BaseLayout.PullDown
AnimatedVisibility(hasPermission == false && requiresAccessibilityService(swipeDown) && !swipeDownIsSearch) {
MissingPermissionBanner(
modifier = Modifier.padding(16.dp),
@@ -149,7 +150,7 @@ fun GestureSettingsScreen() {
)
val swipeLeft by viewModel.swipeLeft.collectAsStateWithLifecycle(null)
val swipeLeftIsSearch = layout == Layout.Pager
val swipeLeftIsSearch = layout == BaseLayout.Pager
AnimatedVisibility(hasPermission == false && requiresAccessibilityService(swipeLeft) && !swipeLeftIsSearch) {
MissingPermissionBanner(
modifier = Modifier.padding(16.dp),
@@ -173,7 +174,7 @@ fun GestureSettingsScreen() {
)
val swipeRight by viewModel.swipeRight.collectAsStateWithLifecycle(null)
val swipeRightIsSearch = layout == Layout.PagerReversed
val swipeRightIsSearch = layout == BaseLayout.PagerReversed
AnimatedVisibility(hasPermission == false && requiresAccessibilityService(swipeRight) && !swipeRightIsSearch) {
MissingPermissionBanner(
modifier = Modifier.padding(16.dp),
@@ -225,11 +226,11 @@ fun GestureSettingsScreen() {
fun requiresAccessibilityService(action: GestureAction?): Boolean {
return when (action) {
GestureAction.OpenNotificationDrawer,
GestureAction.LockScreen,
GestureAction.OpenQuickSettings,
GestureAction.OpenRecents,
GestureAction.OpenPowerDialog -> true
is GestureAction.Notifications,
is GestureAction.ScreenLock,
is GestureAction.QuickSettings,
is GestureAction.Recents,
is GestureAction.PowerMenu -> true
else -> false
}
}
@@ -256,12 +257,12 @@ fun GesturePreference(
title = title,
enabled = !isOpenSearch,
items = options,
value = if (isOpenSearch) GestureAction.OpenSearch else value,
value = if (isOpenSearch) GestureAction.Search else value,
onValueChanged = { if (it != null) onValueChanged(it) }
)
}
if (value == GestureAction.LaunchApp && !isOpenSearch) {
if (value is GestureAction.Launch && !isOpenSearch) {
Box(
modifier = Modifier
.height(36.dp)
@@ -277,12 +278,12 @@ fun GesturePreference(
}
}
if (!isOpenSearch && value == GestureAction.LaunchApp && (showAppPicker || app == null)) {
if (!isOpenSearch && value is GestureAction.Launch && (showAppPicker || app == null)) {
SearchablePicker(
title = { Text(title) },
onDismissRequest = {
showAppPicker = false
if (app == null) onValueChanged(GestureAction.None)
if (app == null) onValueChanged(GestureAction.NoAction)
},
value = app,
onValueChanged = {
@@ -3,15 +3,16 @@ package de.mm20.launcher2.ui.settings.gestures
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.searchable.SavableSearchableRepository
import de.mm20.launcher2.icons.IconService
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.Settings.GestureSettings.GestureAction
import de.mm20.launcher2.preferences.BaseLayout
import de.mm20.launcher2.preferences.GestureAction
import de.mm20.launcher2.preferences.ui.GestureSettings
import de.mm20.launcher2.preferences.ui.UiSettings
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.emptyFlow
@@ -22,7 +23,8 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class GestureSettingsScreenVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val gestureSettings: GestureSettings by inject()
private val uiSettings: UiSettings by inject()
private val permissionsManager: PermissionsManager by inject()
private val searchableRepository: SavableSearchableRepository by inject()
private val iconService: IconService by inject()
@@ -30,208 +32,124 @@ class GestureSettingsScreenVM : ViewModel(), KoinComponent {
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Accessibility)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val baseLayout = dataStore.data.map { it.layout.baseLayout }
val baseLayout = uiSettings.baseLayout
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setBaseLayout(baseLayout: Settings.LayoutSettings.Layout) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setLayout(it.layout.toBuilder().setBaseLayout(baseLayout))
.build()
}
}
fun setBaseLayout(baseLayout: BaseLayout) {
uiSettings.setBaseLayout(baseLayout)
}
val swipeDown = dataStore.data.map { it.gestures.swipeDown }
val swipeDown = gestureSettings.swipeDown
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val swipeLeft = dataStore.data.map { it.gestures.swipeLeft }
val swipeLeft = gestureSettings.swipeLeft
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val swipeRight = dataStore.data.map { it.gestures.swipeRight }
val swipeRight = gestureSettings.swipeRight
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val doubleTap = dataStore.data.map { it.gestures.doubleTap }
val doubleTap = gestureSettings.doubleTap
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val longPress = dataStore.data.map { it.gestures.longPress }
val longPress = gestureSettings.longPress
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val homeButton = dataStore.data.map { it.gestures.homeButton }
val homeButton = gestureSettings.homeButton
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setSwipeDown(action: GestureAction) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder().setGestures(it.gestures.toBuilder().setSwipeDown(action).build())
.build()
}
}
gestureSettings.setSwipeDown(action)
}
fun setSwipeLeft(action: GestureAction) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder().setGestures(it.gestures.toBuilder().setSwipeLeft(action).build())
.build()
}
}
gestureSettings.setSwipeLeft(action)
}
fun setSwipeRight(action: GestureAction) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder().setGestures(it.gestures.toBuilder().setSwipeRight(action).build())
.build()
}
}
gestureSettings.setSwipeRight(action)
}
fun setDoubleTap(action: GestureAction) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder().setGestures(it.gestures.toBuilder().setDoubleTap(action).build())
.build()
}
}
gestureSettings.setDoubleTap(action)
}
fun setLongPress(action: GestureAction) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder().setGestures(it.gestures.toBuilder().setLongPress(action).build())
.build()
}
}
gestureSettings.setLongPress(action)
}
fun setHomeButton(action: GestureAction) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder().setGestures(it.gestures.toBuilder().setHomeButton(action).build())
.build()
}
}
gestureSettings.setHomeButton(action)
}
val swipeLeftApp: Flow<SavableSearchable?> = dataStore.data.map { it.gestures.swipeLeftApp }
val swipeLeftApp: Flow<SavableSearchable?> = swipeLeft
.map {
if (it.isEmpty()) null else searchableRepository.getByKeys(listOf(it)).firstOrNull()
if (it !is GestureAction.Launch || it.key == null) null
else searchableRepository.getByKeys(listOf(it.key!!)).firstOrNull()
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis = 10000), null)
fun setSwipeLeftApp(searchable: SavableSearchable?) {
viewModelScope.launch {
searchable?.let { searchableRepository.insert(it) }
dataStore.updateData {
it.toBuilder()
.setGestures(it.gestures.toBuilder()
.setSwipeLeftApp(searchable?.key ?: "")
.build()
)
.build()
}
}
searchable?.let { searchableRepository.insert(it) } ?: return
setSwipeLeft(GestureAction.Launch(searchable.key))
}
val swipeRightApp: Flow<SavableSearchable?> = dataStore.data.map { it.gestures.swipeRightApp }
val swipeRightApp: Flow<SavableSearchable?> = swipeRight
.map {
if (it.isEmpty()) null else searchableRepository.getByKeys(listOf(it)).firstOrNull()
if (it !is GestureAction.Launch || it.key == null) null
else searchableRepository.getByKeys(listOf(it.key!!)).firstOrNull()
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis = 10000), null)
fun setSwipeRightApp(searchable: SavableSearchable?) {
viewModelScope.launch {
searchable?.let { searchableRepository.insert(it) }
dataStore.updateData {
it.toBuilder()
.setGestures(it.gestures.toBuilder()
.setSwipeRightApp(searchable?.key ?: "")
.build()
)
.build()
}
}
searchable?.let { searchableRepository.insert(it) } ?: return
setSwipeRight(GestureAction.Launch(searchable.key))
}
val swipeDownApp: Flow<SavableSearchable?> = dataStore.data.map { it.gestures.swipeDownApp }
val swipeDownApp: Flow<SavableSearchable?> = swipeDown
.map {
if (it.isEmpty()) null else searchableRepository.getByKeys(listOf(it)).firstOrNull()
if (it !is GestureAction.Launch || it.key == null) null
else searchableRepository.getByKeys(listOf(it.key!!)).firstOrNull()
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis = 10000), null)
fun setSwipeDownApp(searchable: SavableSearchable?) {
viewModelScope.launch {
searchable?.let { searchableRepository.insert(it) }
dataStore.updateData {
it.toBuilder()
.setGestures(it.gestures.toBuilder()
.setSwipeDownApp(searchable?.key ?: "")
.build()
)
.build()
}
}
searchable?.let { searchableRepository.insert(it) } ?: return
setSwipeDown(GestureAction.Launch(searchable.key))
}
val longPressApp: Flow<SavableSearchable?> = dataStore.data.map { it.gestures.longPressApp }
val longPressApp: Flow<SavableSearchable?> = longPress
.map {
if (it.isEmpty()) null else searchableRepository.getByKeys(listOf(it)).firstOrNull()
if (it !is GestureAction.Launch || it.key == null) null
else searchableRepository.getByKeys(listOf(it.key!!)).firstOrNull()
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis = 10000), null)
fun setLongPressApp(searchable: SavableSearchable?) {
viewModelScope.launch {
searchable?.let { searchableRepository.insert(it) }
dataStore.updateData {
it.toBuilder()
.setGestures(it.gestures.toBuilder()
.setLongPressApp(searchable?.key ?: "")
.build()
)
.build()
}
}
searchable?.let { searchableRepository.insert(it) } ?: return
setLongPress(GestureAction.Launch(searchable.key))
}
val doubleTapApp: Flow<SavableSearchable?> = dataStore.data.map { it.gestures.doubleTapApp }
val doubleTapApp: Flow<SavableSearchable?> = doubleTap
.map {
if (it.isEmpty()) null else searchableRepository.getByKeys(listOf(it)).firstOrNull()
if (it !is GestureAction.Launch || it.key == null) null
else searchableRepository.getByKeys(listOf(it.key!!)).firstOrNull()
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis = 10000), null)
fun setDoubleTapApp(searchable: SavableSearchable?) {
viewModelScope.launch {
searchable?.let { searchableRepository.insert(it) }
dataStore.updateData {
it.toBuilder()
.setGestures(it.gestures.toBuilder()
.setDoubleTapApp(searchable?.key ?: "")
.build()
)
.build()
}
}
searchable?.let { searchableRepository.insert(it) } ?: return
setDoubleTap(GestureAction.Launch(searchable.key))
}
val homeButtonApp: Flow<SavableSearchable?> = dataStore.data.map { it.gestures.homeButtonApp }
val homeButtonApp: Flow<SavableSearchable?> = homeButton
.map {
if (it.isEmpty()) null else searchableRepository.getByKeys(listOf(it)).firstOrNull()
if (it !is GestureAction.Launch || it.key == null) null
else searchableRepository.getByKeys(listOf(it.key!!)).firstOrNull()
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis = 10000), null)
fun setHomeButtonApp(searchable: SavableSearchable?) {
viewModelScope.launch {
searchable?.let { searchableRepository.insert(it) }
dataStore.updateData {
it.toBuilder()
.setGestures(it.gestures.toBuilder()
.setHomeButtonApp(searchable?.key ?: "")
.build()
)
.build()
}
}
searchable?.let { searchableRepository.insert(it) } ?: return
setHomeButton(GestureAction.Launch(searchable.key))
}
fun requestPermission(context: AppCompatActivity) {
permissionsManager.requestPermission(context, PermissionGroup.Accessibility)
}
@@ -9,7 +9,7 @@ import de.mm20.launcher2.searchable.SavableSearchableRepository
import de.mm20.launcher2.icons.IconService
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.ui.SearchUiSettings
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.Application
import kotlinx.coroutines.Dispatchers
@@ -29,7 +29,7 @@ class HiddenItemsSettingsScreenVM : ViewModel(), KoinComponent {
private val appRepository: AppRepository by inject()
private val searchableRepository: SavableSearchableRepository by inject()
private val iconService: IconService by inject()
private val dataStore: LauncherDataStore by inject()
private val searchUiSettings: SearchUiSettings by inject()
val allApps = appRepository.findMany().map {
withContext(Dispatchers.Default) { it.sorted() }
@@ -68,16 +68,10 @@ class HiddenItemsSettingsScreenVM : ViewModel(), KoinComponent {
app.openAppDetails(context)
}
val hiddenItemsButton = dataStore.data.map { it.searchBar.hiddenItemsButton }
val hiddenItemsButton = searchUiSettings.hiddenItemsButton
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
fun setHiddenItemsButton(hidden: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setSearchBar(it.searchBar.toBuilder().setHiddenItemsButton(hidden)).build()
}
}
searchUiSettings.setHiddenItemsButton(hidden)
}
}
@@ -26,7 +26,10 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.pager.HorizontalPagerIndicator
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.LegacySettings
import de.mm20.launcher2.preferences.SearchBarColors
import de.mm20.launcher2.preferences.SearchBarStyle
import de.mm20.launcher2.preferences.SystemBarColors
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.SearchBar
import de.mm20.launcher2.ui.component.SearchBarLevel
@@ -109,14 +112,14 @@ fun HomescreenSettingsScreen() {
viewModel.setSearchBarStyle(it)
}
)
AnimatedVisibility(searchBarStyle == Settings.SearchBarSettings.SearchBarStyle.Transparent) {
AnimatedVisibility(searchBarStyle == SearchBarStyle.Transparent) {
ListPreference(
title = stringResource(R.string.preference_search_bar_color),
value = searchBarColor,
items = listOf(
stringResource(R.string.preference_system_bar_icons_auto) to Settings.SearchBarSettings.SearchBarColors.Auto,
stringResource(R.string.preference_system_bar_icons_light) to Settings.SearchBarSettings.SearchBarColors.Light,
stringResource(R.string.preference_system_bar_icons_dark) to Settings.SearchBarSettings.SearchBarColors.Dark,
stringResource(R.string.preference_system_bar_icons_auto) to SearchBarColors.Auto,
stringResource(R.string.preference_system_bar_icons_light) to SearchBarColors.Light,
stringResource(R.string.preference_system_bar_icons_dark) to SearchBarColors.Dark,
),
onValueChanged = {
if (it != null) viewModel.setSearchBarColor(it)
@@ -208,9 +211,9 @@ fun HomescreenSettingsScreen() {
title = stringResource(R.string.preference_status_bar_icons),
value = lightStatusBar,
items = listOf(
stringResource(R.string.preference_system_bar_icons_auto) to Settings.SystemBarsSettings.SystemBarColors.Auto,
stringResource(R.string.preference_system_bar_icons_light) to Settings.SystemBarsSettings.SystemBarColors.Light,
stringResource(R.string.preference_system_bar_icons_dark) to Settings.SystemBarsSettings.SystemBarColors.Dark,
stringResource(R.string.preference_system_bar_icons_auto) to SystemBarColors.Auto,
stringResource(R.string.preference_system_bar_icons_light) to SystemBarColors.Light,
stringResource(R.string.preference_system_bar_icons_dark) to SystemBarColors.Dark,
),
onValueChanged = {
if (it != null) viewModel.setLightStatusBar(it)
@@ -220,9 +223,9 @@ fun HomescreenSettingsScreen() {
title = stringResource(R.string.preference_nav_bar_icons),
value = lightNavBar,
items = listOf(
stringResource(R.string.preference_system_bar_icons_auto) to Settings.SystemBarsSettings.SystemBarColors.Auto,
stringResource(R.string.preference_system_bar_icons_light) to Settings.SystemBarsSettings.SystemBarColors.Light,
stringResource(R.string.preference_system_bar_icons_dark) to Settings.SystemBarsSettings.SystemBarColors.Dark,
stringResource(R.string.preference_system_bar_icons_auto) to SystemBarColors.Auto,
stringResource(R.string.preference_system_bar_icons_light) to SystemBarColors.Light,
stringResource(R.string.preference_system_bar_icons_dark) to SystemBarColors.Dark,
),
onValueChanged = {
if (it != null) viewModel.setLightNavBar(it)
@@ -255,15 +258,14 @@ fun HomescreenSettingsScreen() {
fun SearchBarStylePreference(
title: String,
summary: String? = null,
value: Settings.SearchBarSettings.SearchBarStyle?,
onValueChanged: (Settings.SearchBarSettings.SearchBarStyle) -> Unit
value: SearchBarStyle?,
onValueChanged: (SearchBarStyle) -> Unit
) {
var showDialog by remember { mutableStateOf(false) }
Preference(title = title, summary = summary, onClick = { showDialog = true })
if (showDialog && value != null) {
val styles = remember {
Settings.SearchBarSettings.SearchBarStyle.values()
.filter { it != Settings.SearchBarSettings.SearchBarStyle.UNRECOGNIZED }
SearchBarStyle.entries
}
val pagerState = rememberPagerState(initialPage = styles.indexOf(value)) { styles.size }
@@ -13,8 +13,12 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.LegacySettings
import de.mm20.launcher2.preferences.ScreenOrientation
import de.mm20.launcher2.preferences.SearchBarColors
import de.mm20.launcher2.preferences.SearchBarStyle
import de.mm20.launcher2.preferences.SystemBarColors
import de.mm20.launcher2.preferences.ui.UiSettings
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
@@ -23,52 +27,31 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.get
class HomescreenSettingsScreenVM(
private val dataStore: LauncherDataStore,
private val uiSettings: UiSettings,
) : ViewModel() {
var showClockWidgetSheet by mutableStateOf(false)
val dimWallpaper = dataStore.data.map { it.appearance.dimWallpaper }
val dimWallpaper = uiSettings.dimWallpaper
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
fun setDimWallpaper(dimWallpaper: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setAppearance(
it.appearance.toBuilder()
.setDimWallpaper(dimWallpaper)
).build()
}
}
uiSettings.setDimWallpaper(dimWallpaper)
}
val blurWallpaper = dataStore.data.map { it.appearance.blurWallpaper }
val blurWallpaper = uiSettings.blurWallpaper
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
fun setBlurWallpaper(blurWallpaper: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setAppearance(
it.appearance.toBuilder()
.setBlurWallpaper(blurWallpaper)
).build()
}
}
uiSettings.setBlurWallpaper(blurWallpaper)
}
val blurWallpaperRadius = dataStore.data.map { it.appearance.blurWallpaperRadius }
val blurWallpaperRadius = uiSettings.wallpaperBlurRadius
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 32)
fun setBlurWallpaperRadius(blurWallpaperRadius: Int) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setAppearance(
it.appearance.toBuilder()
.setBlurWallpaperRadius(blurWallpaperRadius)
).build()
}
}
uiSettings.setWallpaperBlurRadius(blurWallpaperRadius)
}
fun openWallpaperChooser(context: AppCompatActivity) {
@@ -80,168 +63,88 @@ class HomescreenSettingsScreenVM(
return context.getSystemService<WindowManager>()?.isCrossWindowBlurEnabled == true
}
val statusBarIcons = dataStore.data.map { it.systemBars.statusBarColor }
val statusBarIcons = uiSettings.statusBarColor
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setLightStatusBar(statusBarColor: Settings.SystemBarsSettings.SystemBarColors) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setSystemBars(
it.systemBars.toBuilder()
.setStatusBarColor(statusBarColor)
)
.build()
}
}
fun setLightStatusBar(statusBarColor: SystemBarColors) {
uiSettings.setStatusBarColor(statusBarColor)
}
val navBarIcons = dataStore.data.map { it.systemBars.navBarColor }
val navBarIcons = uiSettings.navigationBarColor
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setLightNavBar(navBarColors: Settings.SystemBarsSettings.SystemBarColors) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setSystemBars(
it.systemBars.toBuilder()
.setNavBarColor(navBarColors)
)
.build()
}
}
fun setLightNavBar(navBarColors: SystemBarColors) {
uiSettings.setNavigationBarColor(navBarColors)
}
val hideStatusBar = dataStore.data.map { it.systemBars.hideStatusBar }
val hideStatusBar = uiSettings.hideStatusBar
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setHideStatusBar(hideStatusBar: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setSystemBars(
it.systemBars.toBuilder()
.setHideStatusBar(hideStatusBar)
)
.build()
}
}
uiSettings.setHideStatusBar(hideStatusBar)
}
val hideNavBar = dataStore.data.map { it.systemBars.hideNavBar }
val hideNavBar = uiSettings.hideNavigationBar
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setHideNavBar(hideNavBar: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setSystemBars(
it.systemBars.toBuilder()
.setHideNavBar(hideNavBar)
)
.build()
}
}
uiSettings.setHideNavigationBar(hideNavBar)
}
val searchBarColor = dataStore.data.map { it.searchBar.color }
val searchBarColor = uiSettings.searchBarColor
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setSearchBarColor(color: Settings.SearchBarSettings.SearchBarColors) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setSearchBar(
it.searchBar.toBuilder()
.setColor(color)
)
.build()
}
}
fun setSearchBarColor(color: SearchBarColors) {
uiSettings.setSearchBarColor(color)
}
val searchBarStyle = dataStore.data.map { it.searchBar.searchBarStyle }
val searchBarStyle = uiSettings.searchBarStyle
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setSearchBarStyle(searchBarStyle: Settings.SearchBarSettings.SearchBarStyle) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setSearchBar(
it.searchBar.toBuilder()
.setSearchBarStyle(searchBarStyle)
)
.build()
}
}
fun setSearchBarStyle(searchBarStyle: SearchBarStyle) {
uiSettings.setSearchBarStyle(searchBarStyle)
}
val fixedSearchBar = dataStore.data.map { it.layout.fixedSearchBar }
val fixedSearchBar = uiSettings.fixedSearchBar
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setFixedSearchBar(fixedSearchBar: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setLayout(it.layout.toBuilder().setFixedSearchBar(fixedSearchBar))
.build()
}
}
uiSettings.setFixedSearchBar(fixedSearchBar)
}
val bottomSearchBar = dataStore.data.map { it.layout.bottomSearchBar }
val bottomSearchBar = uiSettings.bottomSearchBar
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setBottomSearchBar(bottomSearchBar: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setLayout(it.layout.toBuilder().setBottomSearchBar(bottomSearchBar))
.build()
}
}
uiSettings.setBottomSearchBar(bottomSearchBar)
}
val fixedRotation = dataStore.data.map { it.layout.fixedRotation }
val fixedRotation = uiSettings.orientation.map { it != ScreenOrientation.Auto }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setFixedRotation(fixedRotation: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setLayout(it.layout.toBuilder().setFixedRotation(fixedRotation))
.build()
}
}
uiSettings.setOrientation(if (fixedRotation) ScreenOrientation.Portrait else ScreenOrientation.Auto)
}
val widgetEditButton = dataStore.data.map { it.widgets.editButton }
val widgetEditButton = uiSettings.widgetEditButton
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setWidgetEditButton(editButton: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setWidgets(
it.widgets.toBuilder()
.setEditButton(editButton)
)
.build()
}
}
uiSettings.setWidgetEditButton(editButton)
}
val chargingAnimation = dataStore.data.map { it.animations.charging }
val chargingAnimation = uiSettings.chargingAnimation
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setChargingAnimation(chargingAnimation: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setAnimations(
it.animations.toBuilder()
.setCharging(chargingAnimation)
)
.build()
}
}
uiSettings.setChargingAnimation(chargingAnimation)
}
companion object : KoinComponent {
val Factory = viewModelFactory {
initializer {
HomescreenSettingsScreenVM(
dataStore = get()
uiSettings = get()
)
}
}
@@ -24,7 +24,6 @@ import androidx.compose.material3.FilledIconToggleButton
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.PlainTooltipBox
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@@ -50,7 +49,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.icons.StaticIconLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.IconShape
import de.mm20.launcher2.preferences.ui.GridSettings
import de.mm20.launcher2.preferences.ui.IconSettings
import de.mm20.launcher2.preferences.ui.IconSettingsData
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
@@ -63,25 +65,18 @@ import de.mm20.launcher2.ui.component.preferences.SliderPreference
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.component.preferences.label
import de.mm20.launcher2.ui.component.preferences.value
import de.mm20.launcher2.ui.ktx.toPixels
@Composable
fun IconsSettingsScreen() {
val viewModel: IconsSettingsScreenVM = viewModel(factory = IconsSettingsScreenVM.Factory)
val context = LocalContext.current
val iconSize by viewModel.iconSize.collectAsStateWithLifecycle(48)
val grid by viewModel.grid.collectAsStateWithLifecycle(GridSettings())
val icons by viewModel.icons.collectAsStateWithLifecycle(null)
val density = LocalDensity.current
val showLabels by viewModel.showLabels.collectAsStateWithLifecycle(null)
val columnCount by viewModel.columnCount.collectAsStateWithLifecycle(5)
val iconShape by viewModel.iconShape.collectAsStateWithLifecycle(Settings.IconSettings.IconShape.PlatformDefault)
val adaptifyLegacyIcons by viewModel.adaptifyLegacyIcons.collectAsStateWithLifecycle(null)
val themedIcons by viewModel.themedIcons.collectAsStateWithLifecycle(null)
val iconShape by viewModel.iconShape.collectAsStateWithLifecycle(IconShape.PlatformDefault)
val iconPackPackage by viewModel.iconPack.collectAsStateWithLifecycle(null)
val iconPackThemed by viewModel.iconPackThemed.collectAsState(true)
val installedIconPacks by viewModel.installedIconPacks.collectAsState(emptyList())
val forceThemedIcons by viewModel.forceThemedIcons.collectAsStateWithLifecycle(null)
val hasNotificationsPermission by viewModel.hasNotificationsPermission.collectAsStateWithLifecycle(null)
@@ -91,8 +86,8 @@ fun IconsSettingsScreen() {
val shortcutBadges by viewModel.shortcutBadges.collectAsStateWithLifecycle(null)
val pluginBadges by viewModel.pluginBadges.collectAsStateWithLifecycle(null)
val previewIcons by remember(iconSize) {
viewModel.getPreviewIcons(with(density) { iconSize.dp.toPx() }.toInt())
val previewIcons by remember(grid?.iconSize) {
viewModel.getPreviewIcons(with(density) { grid.iconSize.dp.toPx() }.toInt())
}.collectAsState(
emptyList()
)
@@ -102,7 +97,7 @@ fun IconsSettingsScreen() {
PreferenceCategory(title = stringResource(R.string.preference_category_grid)) {
SliderPreference(
title = stringResource(R.string.preference_grid_icon_size),
value = iconSize,
value = grid.iconSize,
step = 8,
min = 32,
max = 64,
@@ -113,14 +108,14 @@ fun IconsSettingsScreen() {
SwitchPreference(
title = stringResource(R.string.preference_grid_labels),
summary = stringResource(R.string.preference_grid_labels_summary),
value = showLabels == true,
value = grid.showLabels,
onValueChanged = {
viewModel.setShowLabels(it)
}
)
SliderPreference(
title = stringResource(R.string.preference_grid_column_count),
value = columnCount,
value = grid.columnCount,
min = 3,
max = 12,
onValueChanged = {
@@ -148,7 +143,7 @@ fun IconsSettingsScreen() {
modifier = Modifier.weight(1f),
contentAlignment = Alignment.Center
) {
ShapedLauncherIcon(size = iconSize.dp, icon = { icon })
ShapedLauncherIcon(size = grid.iconSize.dp, icon = { icon })
}
}
}
@@ -165,7 +160,7 @@ fun IconsSettingsScreen() {
SwitchPreference(
title = stringResource(R.string.preference_enforce_icon_shape),
summary = stringResource(R.string.preference_enforce_icon_shape_summary),
value = adaptifyLegacyIcons == true,
value = icons?.adaptify == true,
onValueChanged = {
viewModel.setAdaptifyLegacyIcons(it)
}
@@ -173,7 +168,7 @@ fun IconsSettingsScreen() {
SwitchPreference(
title = stringResource(R.string.preference_themed_icons),
summary = stringResource(R.string.preference_themed_icons_summary),
value = themedIcons == true,
value = icons?.themedIcons == true,
onValueChanged = {
viewModel.setThemedIcons(it)
}
@@ -181,14 +176,14 @@ fun IconsSettingsScreen() {
SwitchPreference(
title = stringResource(R.string.preference_force_themed_icons),
summary = stringResource(R.string.preference_force_themed_icons_summary),
value = forceThemedIcons == true,
enabled = themedIcons == true,
value = icons?.forceThemed == true,
enabled = icons?.themedIcons == true,
onValueChanged = {
viewModel.setForceThemedIcons(it)
}
)
val iconPack by remember {
derivedStateOf { installedIconPacks.firstOrNull { it.packageName == iconPackPackage } }
derivedStateOf { installedIconPacks.firstOrNull { it.packageName == icons?.iconPack } }
}
val items = installedIconPacks.map {
it.name to it
@@ -265,7 +260,7 @@ fun IconsSettingsScreen() {
PlainTooltipBox(tooltip = { Text(stringResource(R.string.icon_pack_dynamic_colors)) }) {
FilledIconToggleButton(
modifier = Modifier.tooltipTrigger(),
checked = iconPackThemed,
checked = icons?.iconPackThemed == true,
onCheckedChange = {
viewModel.setIconPackThemed(it)
}) {
@@ -347,16 +342,16 @@ fun IconsSettingsScreen() {
fun IconShapePreference(
title: String,
summary: String? = null,
value: Settings.IconSettings.IconShape?,
onValueChanged: (Settings.IconSettings.IconShape) -> Unit
value: IconShape?,
onValueChanged: (IconShape) -> Unit
) {
var showDialog by remember { mutableStateOf(false) }
Preference(title = title, summary = summary, onClick = { showDialog = true })
if (showDialog && value != null) {
val shapes = remember {
Settings.IconSettings.IconShape.values()
.filter { it != Settings.IconSettings.IconShape.UNRECOGNIZED && it != Settings.IconSettings.IconShape.EasterEgg }
IconShape.entries
.filter { it != IconShape.EasterEgg }
}
Dialog(onDismissRequest = { showDialog = false }) {
Surface(
@@ -431,19 +426,19 @@ fun IconShapePreference(
@Composable
private fun getShapeName(shape: Settings.IconSettings.IconShape?): String? {
private fun getShapeName(shape: IconShape?): String? {
return stringResource(
when (shape) {
Settings.IconSettings.IconShape.Triangle -> R.string.preference_icon_shape_triangle
Settings.IconSettings.IconShape.Hexagon -> R.string.preference_icon_shape_hexagon
Settings.IconSettings.IconShape.RoundedSquare -> R.string.preference_icon_shape_rounded_square
Settings.IconSettings.IconShape.Squircle -> R.string.preference_icon_shape_squircle
Settings.IconSettings.IconShape.Square -> R.string.preference_icon_shape_square
Settings.IconSettings.IconShape.Pentagon -> R.string.preference_icon_shape_pentagon
Settings.IconSettings.IconShape.PlatformDefault -> R.string.preference_icon_shape_platform
Settings.IconSettings.IconShape.Circle -> R.string.preference_icon_shape_circle
Settings.IconSettings.IconShape.Teardrop -> R.string.preference_icon_shape_teardrop
Settings.IconSettings.IconShape.Pebble -> R.string.preference_icon_shape_pebble
IconShape.Triangle -> R.string.preference_icon_shape_triangle
IconShape.Hexagon -> R.string.preference_icon_shape_hexagon
IconShape.RoundedSquare -> R.string.preference_icon_shape_rounded_square
IconShape.Squircle -> R.string.preference_icon_shape_squircle
IconShape.Square -> R.string.preference_icon_shape_square
IconShape.Pentagon -> R.string.preference_icon_shape_pentagon
IconShape.PlatformDefault -> R.string.preference_icon_shape_platform
IconShape.Circle -> R.string.preference_icon_shape_circle
IconShape.Teardrop -> R.string.preference_icon_shape_teardrop
IconShape.Pebble -> R.string.preference_icon_shape_pebble
else -> return null
}
)
@@ -2,124 +2,64 @@ package de.mm20.launcher2.ui.settings.icons
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import de.mm20.launcher2.badges.settings.BadgeSettings
import de.mm20.launcher2.icons.IconPack
import de.mm20.launcher2.icons.IconService
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.preferences.IconShape
import de.mm20.launcher2.preferences.ui.BadgeSettings
import de.mm20.launcher2.preferences.ui.IconSettings
import de.mm20.launcher2.preferences.ui.UiSettings
import de.mm20.launcher2.services.favorites.FavoritesService
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
class IconsSettingsScreenVM(
private val dataStore: LauncherDataStore,
private val uiSettings: UiSettings,
private val iconSettings: IconSettings,
private val badgeSettings: BadgeSettings,
private val iconService: IconService,
private val favoritesService: FavoritesService,
private val permissionsManager: PermissionsManager,
) : ViewModel() {
val grid = uiSettings.gridSettings
val columnCount = dataStore.data.map { it.grid.columnCount }
fun setColumnCount(columnCount: Int) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setGrid(it.grid.toBuilder().setColumnCount(columnCount))
.build()
}
}
uiSettings.setGridColumnCount(columnCount)
}
val iconSize = dataStore.data.map { it.grid.iconSize }
fun setIconSize(iconSize: Int) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setGrid(it.grid.toBuilder().setIconSize(iconSize))
.build()
}
}
uiSettings.setGridIconSize(iconSize)
}
val showLabels = dataStore.data.map { it.grid.showLabels }
fun setShowLabels(showLabels: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setGrid(it.grid.toBuilder().setShowLabels(showLabels))
.build()
}
}
uiSettings.setGridShowLabels(showLabels)
}
val iconShape = dataStore.data.map { it.icons.shape }
fun setIconShape(iconShape: Settings.IconSettings.IconShape) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setIcons(
it.icons.toBuilder()
.setShape(iconShape)
)
.build()
}
}
val iconShape = uiSettings.iconShape
fun setIconShape(iconShape: IconShape) {
uiSettings.setIconShape(iconShape)
}
val adaptifyLegacyIcons = dataStore.data.map { it.icons.adaptify }
val icons = iconSettings
fun setAdaptifyLegacyIcons(adaptify: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setIcons(
it.icons.toBuilder()
.setAdaptify(adaptify)
)
.build()
}
}
iconSettings.setAdaptifyLegacyIcons(adaptify)
}
val themedIcons = dataStore.data.map { it.icons.themedIcons }
fun setThemedIcons(themedIcons: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setIcons(
it.icons.toBuilder()
.setThemedIcons(themedIcons)
)
.build()
}
}
iconSettings.setThemedIcons(themedIcons)
}
val forceThemedIcons = dataStore.data.map { it.icons.forceThemed }
fun setForceThemedIcons(forceThemedIcons: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setIcons(
it.icons.toBuilder()
.setForceThemed(forceThemedIcons)
)
.build()
}
}
iconSettings.setForceThemedIcons(forceThemedIcons)
}
val installedIconPacks: Flow<List<IconPack>> = iconService.getInstalledIconPacks().map {
@@ -132,33 +72,12 @@ class IconsSettingsScreenVM(
) + it
}
val iconPackThemed = dataStore.data.map { it.icons.iconPackThemed }
fun setIconPackThemed(iconPackThemed: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setIcons(
it.icons
.toBuilder()
.setIconPackThemed(iconPackThemed)
)
.build()
}
}
iconSettings.setIconPackThemed(iconPackThemed)
}
val iconPack = dataStore.data.map { it.icons.iconPack }
fun setIconPack(iconPack: String) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setIcons(
it.icons.toBuilder()
.setIconPack(iconPack)
)
.build()
}
}
fun setIconPack(iconPack: String?) {
iconSettings.setIconPack(iconPack?.takeIf { it.isNotBlank() })
}
val hasNotificationsPermission = permissionsManager.hasPermission(PermissionGroup.Notifications)
@@ -193,10 +112,10 @@ class IconsSettingsScreenVM(
}
fun getPreviewIcons(size: Int): Flow<List<LauncherIcon?>> {
return columnCount.flatMapLatest { cols ->
return grid.flatMapLatest { grid ->
favoritesService.getFavorites(
includeTypes = listOf("app"),
limit = cols,
limit = grid.columnCount,
manuallySorted = true,
automaticallySorted = true,
frequentlyUsed = true,
@@ -214,11 +133,12 @@ class IconsSettingsScreenVM(
val Factory = viewModelFactory {
initializer {
IconsSettingsScreenVM(
dataStore = get(),
uiSettings = get(),
iconService = get(),
permissionsManager = get(),
favoritesService = get(),
badgeSettings = get(),
iconSettings = get(),
)
}
}
@@ -11,13 +11,12 @@ import de.mm20.launcher2.ktx.normalize
import de.mm20.launcher2.music.MusicService
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.media.MediaSettings
import de.mm20.launcher2.search.AppProfile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
@@ -29,7 +28,9 @@ class MediaIntegrationSettingsScreenVM : ViewModel(), KoinComponent {
private val musicService: MusicService by inject()
private val appRepository: AppRepository by inject()
private val iconService: IconService by inject()
private val dataStore: LauncherDataStore by inject()
private val mediaSettings: MediaSettings by inject()
val hasPermission =
permissionsManager.hasPermission(PermissionGroup.Notifications)
@@ -52,9 +53,9 @@ class MediaIntegrationSettingsScreenVM : ViewModel(), KoinComponent {
val musicApps = musicService.getInstalledPlayerPackages()
val allApps = appRepository.findMany().first().filter { it.profile == AppProfile.Personal }
.distinctBy { it.componentName.packageName }
val settings = dataStore.data.map { it.musicWidget }.first()
val allowList = settings.allowListList
val denyList = settings.denyListList
val settings = mediaSettings.first()
val allowList = settings.allowList
val denyList = settings.denyList
appList.value = allApps.map {
AppListItem(
@@ -92,19 +93,7 @@ class MediaIntegrationSettingsScreenVM : ViewModel(), KoinComponent {
denyList.add(app.packageName)
}
}
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setMusicWidget(
it.musicWidget.toBuilder()
.clearAllowList()
.addAllAllowList(allowList)
.clearDenyList()
.addAllDenyList(denyList)
)
.build()
}
}
mediaSettings.setLists(allowList.toSet(), denyList.toSet())
}
}
@@ -20,6 +20,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.automirrored.rounded.InsertDriveFile
import androidx.compose.material.icons.automirrored.rounded.OpenInNew
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Error
import androidx.compose.material.icons.rounded.Info
@@ -40,6 +41,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -48,7 +50,9 @@ import coil.compose.AsyncImage
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.plugin.PluginState
import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.locals.LocalNavController
@@ -76,6 +80,11 @@ fun PluginSettingsScreen(pluginId: String) {
minActiveState = Lifecycle.State.RESUMED
)
val weatherPlugins by viewModel.weatherPlugins.collectAsStateWithLifecycle(
emptyList(),
minActiveState = Lifecycle.State.RESUMED
)
val requestPermissionStarter =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
@@ -87,6 +96,10 @@ fun PluginSettingsScreen(pluginId: String) {
null
)
val weatherProviderId by viewModel.weatherProvider.collectAsStateWithLifecycle(
null
)
Scaffold(
topBar = {
TopAppBar(
@@ -286,7 +299,7 @@ fun PluginSettingsScreen(pluginId: String) {
AnimatedVisibility(pluginPackage?.enabled == true && hasPermission == true) {
if (filePlugins.isNotEmpty()) {
PreferenceCategory(
"File search",
stringResource(R.string.plugin_type_filesearch),
iconPadding = false,
) {
for (plugin in filePlugins) {
@@ -294,7 +307,7 @@ fun PluginSettingsScreen(pluginId: String) {
if (state is PluginState.SetupRequired) {
Banner(
modifier = Modifier.padding(16.dp),
text = state.message ?: "You need to setup this plugin first",
text = state.message ?: stringResource(R.string.plugin_state_setup_required),
icon = Icons.Rounded.Info,
primaryAction = {
TextButton(onClick = {
@@ -311,7 +324,7 @@ fun PluginSettingsScreen(pluginId: String) {
} else if (state is PluginState.Error) {
Banner(
modifier = Modifier.padding(16.dp),
text = "This plugin isn't working correctly",
text = stringResource(R.string.plugin_state_error),
icon = Icons.Rounded.Error,
color = MaterialTheme.colorScheme.errorContainer,
)
@@ -334,6 +347,58 @@ fun PluginSettingsScreen(pluginId: String) {
}
}
}
if (weatherPlugins.isNotEmpty()) {
PreferenceCategory(
stringResource(R.string.plugin_type_weather),
iconPadding = false,
) {
for (plugin in weatherPlugins) {
val state = plugin.state
if (state is PluginState.SetupRequired) {
Banner(
modifier = Modifier.padding(16.dp),
text = state.message ?: stringResource(R.string.plugin_state_setup_required),
icon = Icons.Rounded.Info,
primaryAction = {
TextButton(onClick = {
try {
state.setupActivity.send()
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
}) {
Text(stringResource(R.string.plugin_action_setup))
}
}
)
} else if (state is PluginState.Error) {
Banner(
modifier = Modifier.padding(16.dp),
text = stringResource(R.string.plugin_state_error),
icon = Icons.Rounded.Error,
color = MaterialTheme.colorScheme.errorContainer,
)
}
Preference(
title = plugin.plugin.label,
enabled = state is PluginState.Ready && weatherProviderId != plugin.plugin.authority,
iconPadding = false,
summary = if (weatherProviderId != plugin.plugin.authority) {
stringResource(R.string.plugin_weather_provider_enable)
} else {
stringResource(R.string.plugin_weather_provider_enabled)
}
)
}
Preference(
title = stringResource(R.string.widget_config_weather_integration_settings),
icon = Icons.AutoMirrored.Rounded.OpenInNew,
onClick = {
navController?.navigate("settings/integrations/weather")
}
)
}
}
}
}
}
@@ -7,13 +7,14 @@ import android.net.Uri
import android.provider.Settings
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.files.settings.FileSearchSettings
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.plugin.PluginPackage
import de.mm20.launcher2.plugin.PluginState
import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.plugins.PluginService
import de.mm20.launcher2.plugins.PluginWithState
import de.mm20.launcher2.preferences.search.FileSearchSettings
import de.mm20.launcher2.preferences.weather.WeatherSettings
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -30,6 +31,7 @@ import org.koin.core.component.inject
class PluginSettingsScreenVM : ViewModel(), KoinComponent {
private val pluginService by inject<PluginService>()
private val fileSearchSettings: FileSearchSettings by inject()
private val weatherSettings: WeatherSettings by inject()
private var pluginPackageName = MutableStateFlow<String?>(null)
@@ -70,6 +72,11 @@ class PluginSettingsScreenVM : ViewModel(), KoinComponent {
it.filter { it.plugin.type == PluginType.FileSearch }
}
val weatherPlugins = states
.map {
it.filter { it.plugin.type == PluginType.Weather }
}
fun init(pluginId: String) {
this.pluginPackageName.value = pluginId
@@ -101,4 +108,9 @@ class PluginSettingsScreenVM : ViewModel(), KoinComponent {
fun setFileSearchPluginEnabled(authority: String, enabled: Boolean) {
fileSearchSettings.setPluginEnabled(authority, enabled)
}
val weatherProvider = weatherSettings.providerId
fun setWeatherProvider(providerId: String) {
weatherSettings.setProvider(providerId)
}
}
@@ -13,7 +13,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.LegacySettings
import de.mm20.launcher2.preferences.SearchResultOrder
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.component.preferences.*
@@ -232,8 +233,8 @@ fun SearchSettingsScreen() {
ListPreference(
title = stringResource(R.string.preference_search_result_ordering),
items = listOf(
stringResource(R.string.preference_search_result_ordering_alphabetic) to Settings.SearchResultOrderingSettings.Ordering.Alphabetic,
stringResource(R.string.preference_search_result_ordering_weighted) to Settings.SearchResultOrderingSettings.Ordering.Weighted
stringResource(R.string.preference_search_result_ordering_alphabetic) to SearchResultOrder.Alphabetical,
stringResource(R.string.preference_search_result_ordering_weighted) to SearchResultOrder.Weighted
),
value = searchResultOrdering,
onValueChanged = {
@@ -5,10 +5,16 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.SearchResultOrder
import de.mm20.launcher2.preferences.search.CalculatorSearchSettings
import de.mm20.launcher2.preferences.search.CalendarSearchSettings
import de.mm20.launcher2.preferences.search.ContactSearchSettings
import de.mm20.launcher2.preferences.search.ShortcutSearchSettings
import de.mm20.launcher2.preferences.search.UnitConverterSettings
import de.mm20.launcher2.preferences.search.WebsiteSearchSettings
import de.mm20.launcher2.preferences.search.WikipediaSearchSettings
import de.mm20.launcher2.preferences.ui.SearchUiSettings
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@@ -16,35 +22,32 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class SearchSettingsScreenVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val searchUiSettings: SearchUiSettings by inject()
private val contactSearchSettings: ContactSearchSettings by inject()
private val calendarSearchSettings: CalendarSearchSettings by inject()
private val shortcutSearchSettings: ShortcutSearchSettings by inject()
private val wikipediaSearchSettings: WikipediaSearchSettings by inject()
private val websiteSearchSettings: WebsiteSearchSettings by inject()
private val unitConverterSettings: UnitConverterSettings by inject()
private val calculatorSearchSettings: CalculatorSearchSettings by inject()
private val permissionsManager: PermissionsManager by inject()
val favorites = dataStore.data.map { it.favorites.enabled }
val favorites = searchUiSettings.favorites
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setFavorites(favorites: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder().setFavorites(
it.favorites.toBuilder().setEnabled(favorites)
).build()
}
}
searchUiSettings.setFavorites(favorites)
}
val hasContactsPermission = permissionsManager.hasPermission(PermissionGroup.Contacts)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val contacts = dataStore.data.map { it.contactsSearch.enabled }
val contacts = contactSearchSettings.enabled
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setContacts(contacts: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder().setContactsSearch(
it.contactsSearch.toBuilder().setEnabled(contacts)
).build()
}
}
contactSearchSettings.setEnabled(contacts)
}
fun requestContactsPermission(activity: AppCompatActivity) {
@@ -53,133 +56,81 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val calendar = dataStore.data.map { it.calendarSearch.enabled }
val calendar = calendarSearchSettings.enabled
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setCalendar(calendar: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder().setCalendarSearch(
it.calendarSearch.toBuilder().setEnabled(calendar)
).build()
}
}
calendarSearchSettings.setEnabled(calendar)
}
fun requestCalendarPermission(activity: AppCompatActivity) {
permissionsManager.requestPermission(activity, PermissionGroup.Calendar)
}
val calculator = dataStore.data.map { it.calculatorSearch.enabled }
val calculator = calculatorSearchSettings.enabled
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setCalculator(calculator: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder().setCalculatorSearch(
it.calculatorSearch.toBuilder().setEnabled(calculator)
).build()
}
}
calculatorSearchSettings.setEnabled(calculator)
}
val unitConverter = dataStore.data.map { it.unitConverterSearch.enabled }
val unitConverter = unitConverterSettings.enabled
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setUnitConverter(unitConverter: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder().setUnitConverterSearch(
it.unitConverterSearch.toBuilder().setEnabled(unitConverter)
).build()
}
}
unitConverterSettings.setEnabled(unitConverter)
}
val wikipedia = dataStore.data.map { it.wikipediaSearch.enabled }
val wikipedia = wikipediaSearchSettings.enabled
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setWikipedia(wikipedia: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder().setWikipediaSearch(
it.wikipediaSearch.toBuilder().setEnabled(wikipedia)
).build()
}
}
wikipediaSearchSettings.setEnabled(wikipedia)
}
val websites = dataStore.data.map { it.websiteSearch.enabled }
val websites = websiteSearchSettings.enabled
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setWebsites(websites: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder().setWebsiteSearch(
it.websiteSearch.toBuilder().setEnabled(websites)
).build()
}
}
websiteSearchSettings.setEnabled(websites)
}
val autoFocus = dataStore.data.map { it.searchBar.autoFocus }
val autoFocus = searchUiSettings.openKeyboard
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setAutoFocus(autoFocus: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder().setSearchBar(
it.searchBar.toBuilder().setAutoFocus(autoFocus)
).build()
}
}
searchUiSettings.setOpenKeyboard(autoFocus)
}
val launchOnEnter = dataStore.data.map { it.searchBar.launchOnEnter }
val launchOnEnter = searchUiSettings.launchOnEnter
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setLaunchOnEnter(launchOnEnter: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder().setSearchBar(
it.searchBar.toBuilder().setLaunchOnEnter(launchOnEnter)
).build()
}
}
searchUiSettings.setLaunchOnEnter(launchOnEnter)
}
val hasAppShortcutPermission = permissionsManager.hasPermission(PermissionGroup.AppShortcuts)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val appShortcuts = dataStore.data.map { it.appShortcutSearch.enabled }
val appShortcuts = shortcutSearchSettings.enabled
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setAppShortcuts(appShortcuts: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder().setAppShortcutSearch(
it.appShortcutSearch.toBuilder().setEnabled(appShortcuts)
).build()
}
}
shortcutSearchSettings.setEnabled(appShortcuts)
}
val searchResultOrdering = dataStore.data.map { it.resultOrdering.ordering }
val searchResultOrdering = searchUiSettings.resultOrder
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setSearchResultOrdering(searchResultOrdering: Settings.SearchResultOrderingSettings.Ordering) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder().setResultOrdering(
it.resultOrdering.toBuilder().setOrdering(searchResultOrdering)
).build()
}
}
fun setSearchResultOrdering(searchResultOrdering: SearchResultOrder) {
searchUiSettings.setResultOrder(searchResultOrdering)
}
val reverseSearchResults = dataStore.data.map { it.layout.reverseSearchResults }
val reverseSearchResults = searchUiSettings.reversedResults
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setReverseSearchResults(reverseSearchResults: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setLayout(it.layout.toBuilder().setReverseSearchResults(reverseSearchResults))
.build()
}
}
searchUiSettings.setReversedResults(reverseSearchResults)
}
fun requestAppShortcutsPermission(activity: AppCompatActivity) {
@@ -55,7 +55,7 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
viewModelScope.launch(Dispatchers.Default) {
allTags = tagService.getAllTags().first().toSet()
val items = if (tag != null) tagService.getTaggedItems(tag).first() else emptyList()
val apps = appRepository.findMany().first().sorted()
val apps = appRepository.findMany().first { it.isNotEmpty() }.sorted()
taggedItems = items
taggableApps = apps.map { app -> TaggableItem(app, items.any { app.key == it.key }) }
taggableOther = items.mapNotNull { item ->
@@ -2,7 +2,7 @@ package de.mm20.launcher2.ui.settings.unitconverter
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.search.UnitConverterSettings
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
@@ -12,35 +12,17 @@ import org.koin.core.component.inject
class UnitConverterSettingsScreenVM: ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val settings: UnitConverterSettings by inject()
val unitConverter = dataStore.data.map { it.unitConverterSearch.enabled }
val unitConverter = settings.enabled
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setUnitConverter(unitConverter: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setUnitConverterSearch(
it.unitConverterSearch.toBuilder()
.setEnabled(unitConverter)
)
.build()
}
}
settings.setEnabled(unitConverter)
}
val currencyConverter = dataStore.data.map { it.unitConverterSearch.currencies }
val currencyConverter = settings.currencies
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setCurrencyConverter(currencyConverter: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setUnitConverterSearch(
it.unitConverterSearch.toBuilder()
.setCurrencies(currencyConverter)
)
.build()
}
}
settings.setCurrencies(currencyConverter)
}
}
@@ -1,21 +1,29 @@
package de.mm20.launcher2.ui.settings.weather
import android.app.PendingIntent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.plugin.PluginState
import de.mm20.launcher2.ui.BuildConfig
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.common.WeatherLocationSearchDialog
import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.component.preferences.*
import de.mm20.launcher2.weather.WeatherLocation
import de.mm20.launcher2.weather.WeatherProviderInfo
@Composable
@@ -25,12 +33,36 @@ fun WeatherIntegrationSettingsScreen() {
val availableProviders by viewModel.availableProviders.collectAsState(emptyList())
val pluginState by viewModel.weatherProviderPluginState.collectAsStateWithLifecycle(
null,
minActiveState = Lifecycle.State.RESUMED
)
PreferenceScreen(
title = stringResource(R.string.preference_screen_weatherwidget),
helpUrl = "https://kvaesitso.mm20.de/docs/user-guide/integrations/weather"
) {
item {
PreferenceCategory {
val state = pluginState?.state
if (state is PluginState.SetupRequired) {
Banner(
modifier = Modifier.padding(16.dp),
text = state.message ?: stringResource(R.string.plugin_state_setup_required),
icon = Icons.Rounded.Info,
primaryAction = {
TextButton(onClick = {
try {
state.setupActivity.send()
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
}) {
Text(stringResource(R.string.plugin_action_setup))
}
}
)
}
val weatherProvider by viewModel.weatherProvider.collectAsState()
ListPreference(
title = stringResource(R.string.preference_weather_provider),
@@ -1,20 +1,15 @@
package de.mm20.launcher2.ui.settings.weather
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.weather.WeatherLocation
import de.mm20.launcher2.weather.WeatherProviderInfo
import de.mm20.launcher2.plugins.PluginService
import de.mm20.launcher2.preferences.weather.WeatherSettings
import de.mm20.launcher2.weather.WeatherRepository
import de.mm20.launcher2.weather.settings.WeatherSettings
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.SharingStarted
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
@@ -25,26 +20,24 @@ import org.koin.core.component.inject
class WeatherIntegrationSettingsScreenVM : ViewModel(), KoinComponent {
private val repository: WeatherRepository by inject()
private val weatherSettings: WeatherSettings by inject()
private val pluginService: PluginService by inject()
private val permissionsManager: PermissionsManager by inject()
private val dataStore: LauncherDataStore by inject()
val availableProviders = repository.getProviders()
val weatherProvider = weatherSettings.providerId
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setWeatherProvider(provider: String) {
weatherSettings.setProviderId(provider)
weatherSettings.setProvider(provider)
}
val imperialUnits = dataStore.data.map { it.weather.imperialUnits }
val weatherProviderPluginState = weatherProvider.flatMapLatest {
it?.let { pluginService.getPluginWithState(it) } ?: flowOf(null)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val imperialUnits = weatherSettings.imperialUnits
fun setImperialUnits(imperialUnits: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setWeather(it.weather.toBuilder().setImperialUnits(imperialUnits))
.build()
}
}
weatherSettings.setImperialUnits(imperialUnits)
}
val autoLocation = weatherSettings.autoLocation
@@ -24,20 +24,10 @@ fun WikipediaSettingsScreen() {
viewModel.setWikipedia(it)
}
)
val images by viewModel.images.collectAsState()
SwitchPreference(
title = stringResource(R.string.preference_search_wikipedia_pictures),
summary = stringResource(R.string.preference_search_wikipedia_pictures_summary),
enabled = wikipedia == true,
value = images == true,
onValueChanged = {
viewModel.setImages(it)
}
)
val customUrl by viewModel.customUrl.collectAsState()
TextPreference(
title = stringResource(R.string.preference_wikipedia_customurl),
value = customUrl,
value = customUrl ?: "",
placeholder = stringResource(id = R.string.wikipedia_url),
summary = customUrl.takeIf { !it.isNullOrBlank() }
?: stringResource(id = R.string.wikipedia_url),
@@ -2,7 +2,7 @@ package de.mm20.launcher2.ui.settings.wikipedia
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.search.WikipediaSearchSettings
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
@@ -11,50 +11,17 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class WikipediaSettingsScreenVM: ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val wikipediaSearchSettings: WikipediaSearchSettings by inject()
val wikipedia = dataStore.data.map { it.wikipediaSearch.enabled }
val wikipedia = wikipediaSearchSettings.enabled
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setWikipedia(wikipedia: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setWikipediaSearch(
it.wikipediaSearch.toBuilder()
.setEnabled(wikipedia)
)
.build()
}
}
wikipediaSearchSettings.setEnabled(wikipedia)
}
val images = dataStore.data.map { it.wikipediaSearch.images }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setImages(images: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setWikipediaSearch(
it.wikipediaSearch.toBuilder()
.setImages(images)
)
.build()
}
}
}
val customUrl = dataStore.data.map { it.wikipediaSearch.customUrl }
val customUrl = wikipediaSearchSettings.customUrl
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "")
fun setCustomUrl(customUrl: String) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setWikipediaSearch(
it.wikipediaSearch.toBuilder()
.setCustomUrl(customUrl)
)
.build()
}
}
wikipediaSearchSettings.setCustomUrl(customUrl)
}
}
@@ -1,23 +1,18 @@
package de.mm20.launcher2.ui.theme
import android.content.Context
import android.os.Build
import android.util.Log
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.coerceAtMost
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.Settings.AppearanceSettings
import de.mm20.launcher2.themes.DefaultThemeId
import de.mm20.launcher2.themes.Theme
import de.mm20.launcher2.preferences.Font
import de.mm20.launcher2.preferences.SurfaceShape
import de.mm20.launcher2.preferences.ui.UiSettings
import de.mm20.launcher2.themes.ThemeRepository
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.theme.colorscheme.*
@@ -26,7 +21,7 @@ import de.mm20.launcher2.ui.theme.typography.getDeviceDefaultTypography
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import org.koin.androidx.compose.inject
import java.util.UUID
import de.mm20.launcher2.preferences.ColorScheme as ColorSchemePref
@Composable
@@ -35,33 +30,31 @@ fun LauncherTheme(
) {
val context = LocalContext.current
val dataStore: LauncherDataStore by inject()
val uiSettings: UiSettings by inject()
val themeRepository: ThemeRepository by inject()
val theme by remember {
dataStore.data.map {
it.appearance.themeId.takeIf { it.isNotEmpty() }?.let {
UUID.fromString(it)
}
}.flatMapLatest {
uiSettings.theme.flatMapLatest {
themeRepository.getThemeOrDefault(it)
}
}.collectAsState(themeRepository.getDefaultTheme())
val themePreference by remember { dataStore.data.map { it.appearance.theme } }.collectAsState(
AppearanceSettings.Theme.System
val colorSchemePref by remember { uiSettings.colorScheme }.collectAsState(
ColorSchemePref.System
)
val darkTheme =
themePreference == AppearanceSettings.Theme.Dark || themePreference == AppearanceSettings.Theme.System && isSystemInDarkTheme()
colorSchemePref == ColorSchemePref.Dark || colorSchemePref == ColorSchemePref.System && isSystemInDarkTheme()
val cornerRadius by remember {
dataStore.data.map { it.cards.radius.dp }
uiSettings.cardStyle.map {
it.cornerRadius.dp
}
}.collectAsState(8.dp)
val baseShape by remember {
dataStore.data.map {
when (it.cards.shape) {
Settings.CardSettings.Shape.Cut -> CutCornerShape(0f)
uiSettings.cardStyle.map {
when (it.shape) {
SurfaceShape.Cut -> CutCornerShape(0f)
else -> RoundedCornerShape(0f)
}
}
@@ -73,8 +66,8 @@ fun LauncherTheme(
lightColorSchemeOf(theme)
}
val font by remember { dataStore.data.map { it.appearance.font } }.collectAsState(
AppearanceSettings.Font.Outfit
val font by remember { uiSettings.font }.collectAsState(
Font.Outfit
)
val typography = remember(font) {
@@ -99,9 +92,9 @@ fun LauncherTheme(
}
}
fun getTypography(context: Context, font: AppearanceSettings.Font?): Typography {
fun getTypography(context: Context, font: Font?): Typography {
return when (font) {
AppearanceSettings.Font.SystemDefault -> getDeviceDefaultTypography(context)
Font.System -> getDeviceDefaultTypography(context)
else -> DefaultTypography
}
}
@@ -11,7 +11,8 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.datastore.core.DataStore
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.LegacySettings
import de.mm20.launcher2.preferences.ui.UiSettings
import de.mm20.launcher2.themes.CorePalette
import de.mm20.launcher2.themes.DefaultDarkColorScheme
import de.mm20.launcher2.themes.DefaultLightColorScheme
@@ -23,6 +24,7 @@ import de.mm20.launcher2.themes.merge
import de.mm20.launcher2.ui.locals.LocalWallpaperColors
import kotlinx.coroutines.flow.map
import org.koin.androidx.compose.inject
import org.koin.core.component.inject
@Composable
fun lightColorSchemeOf(theme: Theme): ColorScheme {
@@ -82,12 +84,12 @@ fun colorSchemeOf(colorScheme: FullColorScheme, corePalette: PartialCorePalette)
@Composable
fun systemCorePalette(): CorePalette<Int> {
val dataStore: DataStore<Settings> by inject()
val compatMode by remember {
dataStore.data.map { it.appearance.forceCompatModeSystemColors }
val uiSettings: UiSettings by inject()
val compatModeColors by remember {
uiSettings.compatModeColors
}.collectAsState(false)
if (Build.VERSION.SDK_INT >= 31 && !compatMode) {
if (Build.VERSION.SDK_INT >= 31 && !compatModeColors) {
val context = LocalContext.current
return CorePalette(
primary = ContextCompat.getColor(context, android.R.color.system_accent1_500),
@@ -112,7 +114,7 @@ fun systemCorePalette(): CorePalette<Int> {
}
}
fun CustomColorScheme(colors: Settings.AppearanceSettings.CustomColors.Scheme): ColorScheme {
fun CustomColorScheme(colors: LegacySettings.AppearanceSettings.CustomColors.Scheme): ColorScheme {
return ColorScheme(
primary = Color(colors.primary),
onPrimary = Color(colors.onPrimary),