Refactor widgets

This commit is contained in:
MM20
2023-04-10 01:15:27 +02:00
parent 2054386179
commit 3cda75bc77
37 changed files with 1153 additions and 347 deletions
+1
View File
@@ -156,6 +156,7 @@ dependencies {
implementation(project(":core:database"))
implementation(project(":data:search-actions"))
implementation(project(":services:global-actions"))
implementation(project(":services:widgets"))
// Uncomment this if you want annoying notifications in your debug builds yelling at you how terrible your code is
//debugImplementation(libs.leakcanary)
@@ -30,6 +30,7 @@ import de.mm20.launcher2.permissions.permissionsModule
import de.mm20.launcher2.preferences.preferencesModule
import de.mm20.launcher2.searchactions.searchActionsModule
import de.mm20.launcher2.services.tags.servicesTagsModule
import de.mm20.launcher2.services.widgets.widgetsServiceModule
import de.mm20.launcher2.weather.weatherModule
import kotlinx.coroutines.*
import org.koin.android.ext.koin.androidContext
@@ -81,6 +82,7 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory {
widgetsModule,
wikipediaModule,
servicesTagsModule,
widgetsServiceModule,
)
)
}
+1
View File
@@ -150,4 +150,5 @@ dependencies {
implementation(project(":services:backup"))
implementation(project(":data:search-actions"))
implementation(project(":services:global-actions"))
implementation(project(":services:widgets"))
}
@@ -8,6 +8,7 @@ import de.mm20.launcher2.ui.component.ProvideIconShape
import de.mm20.launcher2.ui.locals.LocalCardStyle
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
import de.mm20.launcher2.ui.locals.LocalGridSettings
import de.mm20.launcher2.widgets.FavoritesWidget
import de.mm20.launcher2.widgets.WidgetRepository
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -35,7 +36,7 @@ fun ProvideSettings(
val favoritesEnabled by remember {
combine(
widgetRepository.isFavoritesWidgetEnabled(),
widgetRepository.exists(FavoritesWidget.Type),
dataStore.data.map { it.favorites.enabled },
dataStore.data.map { it.clockWidget.favoritesPart },
) { a, b, c -> a || b || c }.distinctUntilChanged()
@@ -8,6 +8,7 @@ import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.Tag
import de.mm20.launcher2.widgets.CalendarWidget
import de.mm20.launcher2.widgets.WidgetRepository
import kotlinx.coroutines.flow.*
import org.koin.core.component.KoinComponent
@@ -36,7 +37,7 @@ 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.isCalendarWidgetEnabled()
val excludeCalendar = widgetRepository.exists(CalendarWidget.Type)
val includeFrequentlyUsed = dataStore.data.map { it.favorites.frequentlyUsed }
val frequentlyUsedRows = dataStore.data.map { it.favorites.frequentlyUsedRows }
@@ -61,11 +61,13 @@ import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.launcher.widgets.picker.PickAppWidgetActivity
import de.mm20.launcher2.widgets.CalendarWidget
import de.mm20.launcher2.widgets.ExternalWidget
import de.mm20.launcher2.widgets.AppWidget
import de.mm20.launcher2.widgets.AppWidgetConfig
import de.mm20.launcher2.widgets.FavoritesWidget
import de.mm20.launcher2.widgets.MusicWidget
import de.mm20.launcher2.widgets.WeatherWidget
import de.mm20.launcher2.widgets.Widget
import java.util.UUID
import kotlin.math.roundToInt
class BindAndConfigureAppWidgetActivity : Activity() {
@@ -191,9 +193,12 @@ private class BindAndConfigureAppWidgetContract(
)
if (widgetId != null && widgetProviderInfo != null) {
return ExternalWidget(
height = widgetProviderInfo.minHeight,
widgetId = widgetId,
return AppWidget(
id = UUID.randomUUID(),
config = AppWidgetConfig(
height = widgetProviderInfo.minHeight,
widgetId = widgetId,
),
widgetProviderInfo = widgetProviderInfo,
)
}
@@ -277,7 +282,15 @@ fun WidgetPickerSheet(
.fillMaxWidth()
.padding(bottom = 16.dp, start = 16.dp, end = 16.dp),
onClick = {
viewModel.pickWidget(it)
val id = UUID.randomUUID()
val widget = when(it.type) {
WeatherWidget.Type -> WeatherWidget(id)
CalendarWidget.Type -> CalendarWidget(id)
MusicWidget.Type -> MusicWidget(id)
FavoritesWidget.Type -> FavoritesWidget(id)
else -> return@OutlinedCard
}
viewModel.pickWidget(widget)
onDismiss()
}) {
Row(
@@ -285,18 +298,18 @@ fun WidgetPickerSheet(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = when (it) {
is WeatherWidget -> Icons.Rounded.LightMode
is CalendarWidget -> Icons.Rounded.Today
is MusicWidget -> Icons.Rounded.MusicNote
is FavoritesWidget -> Icons.Rounded.Star
imageVector = when (it.type) {
WeatherWidget.Type -> Icons.Rounded.LightMode
CalendarWidget.Type -> Icons.Rounded.Today
MusicWidget.Type -> Icons.Rounded.MusicNote
FavoritesWidget.Type -> Icons.Rounded.Star
else -> Icons.Rounded.Widgets
},
contentDescription = null,
modifier = Modifier.padding(end = 16.dp)
)
Text(
text = it.loadLabel(context),
text = it.label,
style = MaterialTheme.typography.titleSmall
)
}
@@ -1,14 +1,21 @@
package de.mm20.launcher2.ui.launcher.sheets
import WidgetsService
import android.appwidget.AppWidgetProviderInfo
import android.content.Context
import android.content.pm.PackageManager
import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Star
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import de.mm20.launcher2.ktx.normalize
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.widgets.FavoritesWidget
import de.mm20.launcher2.widgets.Widget
import de.mm20.launcher2.widgets.WidgetRepository
import kotlinx.coroutines.Dispatchers
@@ -24,7 +31,7 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.get
class WidgetPickerSheetVM(
private val widgetRepository: WidgetRepository,
private val widgetsService: WidgetsService,
private val context: Context,
) : ViewModel() {
@@ -32,11 +39,11 @@ class WidgetPickerSheetVM(
val searchQuery = MutableStateFlow("")
private val enabledWidgets = widgetRepository.getWidgets()
private val enabledWidgets = widgetsService.getWidgets()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(100), emptyList())
private val allBuiltInWidgets = enabledWidgets.map { w ->
widgetRepository.getInternalWidgets().filter { !w.contains(it) }
widgetsService.getBuiltInWidgets().filter { b -> !w.any { it::class == b::class } }
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(100))
val builtInWidgets = allBuiltInWidgets
@@ -45,13 +52,13 @@ class WidgetPickerSheetVM(
withContext(Dispatchers.IO) {
val normalizedQuery = query.normalize()
widgets.filter {
it.loadLabel(context).normalize().contains(normalizedQuery)
it.label.normalize().contains(normalizedQuery)
}
}
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(100))
private val allAppWidgets = flow {
val widgets = widgetRepository.getAppWidgets()
val widgets = widgetsService.getAppWidgetProviders()
emit(widgets)
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(100))
@@ -105,7 +112,7 @@ class WidgetPickerSheetVM(
fun pickWidget(widget: Widget) {
val position = enabledWidgets.value.size
widgetRepository.addWidget(widget, position)
widgetsService.addWidget(widget, position)
}
fun toggleGroup(group: String) {
@@ -123,10 +130,17 @@ class WidgetPickerSheetVM(
}
}
}
}
data class AppWidgetGroup(
val appName: String,
val packageName: String,
val widgets: List<AppWidgetProviderInfo>
)
data class BuiltInWidgetInfo(
val type: String,
@StringRes val label: Int,
val icon: ImageVector
)
@@ -3,32 +3,19 @@ package de.mm20.launcher2.ui.launcher.widgets
import android.app.Activity
import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetManager
import android.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.graphics.res.animatedVectorResource
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
import androidx.compose.animation.graphics.vector.AnimatedImageVector
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -37,7 +24,6 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onPlaced
@@ -45,19 +31,15 @@ import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.ktx.animateTo
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget
import de.mm20.launcher2.ui.launcher.widgets.picker.PickAppWidgetActivity
import de.mm20.launcher2.widgets.ExternalWidget
import de.mm20.launcher2.widgets.AppWidget
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch
@@ -85,19 +67,6 @@ fun WidgetColumn(
}
}
val pickWidgetLauncher =
rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) {
val data = it.data ?: return@rememberLauncherForActivityResult
val widgetId = data.getIntExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID
)
if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return@rememberLauncherForActivityResult
if (it.resultCode == Activity.RESULT_OK) {
viewModel.addAppWidget(context, widgetId)
}
}
Column(
modifier = modifier
) {
@@ -109,7 +78,7 @@ fun WidgetColumn(
}
val widgetsWithIndex = remember(widgets) { widgets.withIndex() }
for ((i, widget) in widgetsWithIndex) {
key(if (widget is ExternalWidget) widget.widgetId else widget) {
key(widget.id) {
var dragOffsetAfterSwap = remember<Float?> { null }
val offsetY = remember(widgets) { mutableStateOf(dragOffsetAfterSwap ?: 0f) }
@@ -122,13 +91,13 @@ fun WidgetColumn(
appWidgetHost = widgetHost,
editMode = editMode,
onWidgetRemove = {
if (widget is ExternalWidget) {
widgetHost.deleteAppWidgetId(widget.widgetId)
if (widget is AppWidget) {
widgetHost.deleteAppWidgetId(widget.config.widgetId)
}
viewModel.removeWidget(widget)
},
onWidgetResize = {
viewModel.setWidgetHeight(widget, it)
onWidgetUpdate = {
viewModel.updateWidget(it)
},
modifier = Modifier
.fillMaxWidth()
@@ -2,6 +2,7 @@ package de.mm20.launcher2.ui.launcher.widgets
import android.app.Activity
import android.appwidget.AppWidgetHost
import android.util.Log
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.gestures.*
@@ -29,7 +30,6 @@ import de.mm20.launcher2.ui.launcher.widgets.favorites.FavoritesWidget
import de.mm20.launcher2.ui.launcher.widgets.music.MusicWidget
import de.mm20.launcher2.ui.launcher.widgets.weather.WeatherWidget
import de.mm20.launcher2.widgets.*
import java.lang.Integer.max
import kotlin.math.roundToInt
@Composable
@@ -38,7 +38,7 @@ fun WidgetItem(
appWidgetHost: AppWidgetHost,
modifier: Modifier = Modifier,
editMode: Boolean = false,
onWidgetResize: (newHeight: Int) -> Unit = {},
onWidgetUpdate: (widget: Widget) -> Unit = {},
onWidgetRemove: () -> Unit = {},
draggableState: DraggableState = rememberDraggableState {},
onDragStopped: () -> Unit = {}
@@ -84,7 +84,7 @@ fun WidgetItem(
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
if (widget is ExternalWidget) {
if (widget is AppWidget) {
IconButton(onClick = { resizeMode = !resizeMode }) {
Icon(
imageVector = Icons.Rounded.Edit,
@@ -124,19 +124,19 @@ fun WidgetItem(
is FavoritesWidget -> {
FavoritesWidget()
}
is ExternalWidget -> {
var height by remember(widget) { mutableStateOf(widget.height) }
is AppWidget -> {
var dragDelta by remember { mutableStateOf(0) }
Column {
ExternalWidget(
appWidgetHost = appWidgetHost,
widgetId = widget.widgetId,
widgetId = widget.config.widgetId,
modifier = Modifier.fillMaxWidth(),
height = height,
height = widget.config.height + dragDelta,
)
if (resizeMode) {
val density = LocalDensity.current
val drgStt = rememberDraggableState {
height += (it / density.density).roundToInt()
dragDelta += (it / density.density).roundToInt()
}
Icon(
imageVector = Icons.Rounded.DragHandle,
@@ -150,7 +150,12 @@ fun WidgetItem(
orientation = Orientation.Vertical,
startDragImmediately = true,
onDragStopped = {
onWidgetResize(height)
onWidgetUpdate(widget.copy(
config = widget.config.copy(
height = widget.config.height + dragDelta
)
))
dragDelta = 0
}
)
)
@@ -1,11 +1,9 @@
package de.mm20.launcher2.ui.launcher.widgets
import android.appwidget.AppWidgetManager
import android.content.Context
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.widgets.ExternalWidget
import de.mm20.launcher2.widgets.Widget
import de.mm20.launcher2.widgets.WidgetRepository
import kotlinx.coroutines.flow.map
@@ -19,49 +17,28 @@ class WidgetsVM : ViewModel(), KoinComponent {
val editButton = dataStore.data.map { it.widgets.editButton }.asLiveData()
val widgets = widgetRepository.getWidgets().asLiveData()
val widgets = widgetRepository.get().asLiveData()
fun addWidget(widget: Widget) {
widgetRepository.addWidget(widget, widgets.value?.size ?: 0)
}
fun addAppWidget(context: Context, widgetId: Int) {
if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return
val appWidget = AppWidgetManager.getInstance(context)
.getAppWidgetInfo(widgetId) ?: return
val widget = ExternalWidget(
widgetProviderInfo = appWidget,
height = appWidget.minHeight,
widgetId = widgetId,
)
addWidget(widget)
}
fun removeWidget(widget: Widget) {
widgetRepository.removeWidget(widget)
widgetRepository.delete(widget)
}
fun setWidgetHeight(widget: Widget, newHeight: Int) {
widgetRepository.setWidgetHeight(widget, newHeight)
}
fun getAvailableBuiltInWidgets(): List<Widget> {
return widgetRepository.getInternalWidgets().filter {
widgets.value?.contains(it)?.not() ?: false
}
fun updateWidget(widget: Widget) {
widgetRepository.update(widget)
}
fun moveUp(index: Int) {
val widgets = widgets.value?.toMutableList() ?: return
val widget = widgets.removeAt(index)
widgets.add(index - 1, widget)
widgetRepository.saveWidgets(widgets)
widgetRepository.set(widgets)
}
fun moveDown(index: Int) {
val widgets = widgets.value?.toMutableList() ?: return
val widget = widgets.removeAt(index)
widgets.add(index + 1, widget)
widgetRepository.saveWidgets(widgets)
widgetRepository.set(widgets)
}
}
@@ -16,6 +16,7 @@ import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
import de.mm20.launcher2.widgets.CalendarWidget
import de.mm20.launcher2.widgets.WidgetRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
@@ -41,7 +42,7 @@ class FavoritesPartProvider : PartProvider, KoinComponent {
if (layout == ClockWidgetLayout.Horizontal) c - 2 else c
}
}.collectAsState(0)
val excludeCalendar by remember { widgetRepository.isCalendarWidgetEnabled() }.collectAsState(
val excludeCalendar by remember { widgetRepository.exists(CalendarWidget.Type) }.collectAsState(
true
)
@@ -1,5 +1,6 @@
package de.mm20.launcher2.ui.launcher.widgets.favorites
import WidgetsService
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout
import de.mm20.launcher2.ui.common.FavoritesVM
@@ -9,13 +10,16 @@ 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 isTopWidget = widgetRepository.isFavoritesWidgetFirst()
private val isTopWidget = widgetsService.isFavoritesWidgetFirst()
private val clockWidgetFavSlots = dataStore.data.combine(isTopWidget) { data, isTop ->
if (!isTop || !data.clockWidget.favoritesPart) 0
else {
@@ -4,6 +4,10 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.widgets.CalendarWidget
import de.mm20.launcher2.widgets.FavoritesWidget
import de.mm20.launcher2.widgets.MusicWidget
import de.mm20.launcher2.widgets.WeatherWidget
import de.mm20.launcher2.widgets.WidgetRepository
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@@ -16,10 +20,10 @@ class WidgetSettingsScreenVM : ViewModel(), KoinComponent {
private val widgetRepository: WidgetRepository by inject()
private val dataStore: LauncherDataStore by inject()
val calendarWidget = widgetRepository.isCalendarWidgetEnabled().asLiveData()
val musicWidget = widgetRepository.isMusicWidgetEnabled().asLiveData()
val weatherWidget = widgetRepository.isWeatherWidgetEnabled().asLiveData()
val favoritesWidget = widgetRepository.isFavoritesWidgetEnabled().asLiveData()
val calendarWidget = widgetRepository.exists(CalendarWidget.Type).asLiveData()
val musicWidget = widgetRepository.exists(MusicWidget.Type).asLiveData()
val weatherWidget = widgetRepository.exists(WeatherWidget.Type).asLiveData()
val favoritesWidget = widgetRepository.exists(FavoritesWidget.Type).asLiveData()
val editButton = dataStore.data.map { it.widgets.editButton }.asLiveData()
fun setEditButton(editButton: Boolean) {
viewModelScope.launch {