Refactor widgets
This commit is contained in:
parent
2054386179
commit
3cda75bc77
@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
kotlin("plugin.serialization") version libs.versions.kotlin apply false
|
||||
id("org.jetbrains.kotlin.android") version libs.versions.kotlin apply false
|
||||
}
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
|
||||
@ -0,0 +1,493 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 23,
|
||||
"identityHash": "cb1e4fa1db90a1afddc085714570430d",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "forecasts",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `temperature` REAL NOT NULL, `minTemp` REAL NOT NULL, `maxTemp` REAL NOT NULL, `pressure` REAL NOT NULL, `humidity` REAL NOT NULL, `icon` INTEGER NOT NULL, `condition` TEXT NOT NULL, `clouds` INTEGER NOT NULL, `windSpeed` REAL NOT NULL, `windDirection` REAL NOT NULL, `rain` REAL NOT NULL, `snow` REAL NOT NULL, `night` INTEGER NOT NULL, `location` TEXT NOT NULL, `provider` TEXT NOT NULL, `providerUrl` TEXT NOT NULL, `rainProbability` INTEGER NOT NULL, `snowProbability` INTEGER NOT NULL, `updateTime` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "timestamp",
|
||||
"columnName": "timestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "temperature",
|
||||
"columnName": "temperature",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "minTemp",
|
||||
"columnName": "minTemp",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "maxTemp",
|
||||
"columnName": "maxTemp",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pressure",
|
||||
"columnName": "pressure",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "humidity",
|
||||
"columnName": "humidity",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "icon",
|
||||
"columnName": "icon",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "condition",
|
||||
"columnName": "condition",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "clouds",
|
||||
"columnName": "clouds",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "windSpeed",
|
||||
"columnName": "windSpeed",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "windDirection",
|
||||
"columnName": "windDirection",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "precipitation",
|
||||
"columnName": "rain",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "snow",
|
||||
"columnName": "snow",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "night",
|
||||
"columnName": "night",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "location",
|
||||
"columnName": "location",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "provider",
|
||||
"columnName": "provider",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "providerUrl",
|
||||
"columnName": "providerUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "precipProbability",
|
||||
"columnName": "rainProbability",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "snowProbability",
|
||||
"columnName": "snowProbability",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updateTime",
|
||||
"columnName": "updateTime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"timestamp"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "Searchable",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `type` TEXT NOT NULL, `searchable` TEXT NOT NULL, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `weight` REAL NOT NULL DEFAULT 0.0, PRIMARY KEY(`key`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "key",
|
||||
"columnName": "key",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serializedSearchable",
|
||||
"columnName": "searchable",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "launchCount",
|
||||
"columnName": "launchCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pinPosition",
|
||||
"columnName": "pinned",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "hidden",
|
||||
"columnName": "hidden",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "weight",
|
||||
"columnName": "weight",
|
||||
"affinity": "REAL",
|
||||
"notNull": true,
|
||||
"defaultValue": "0.0"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"key"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "Currency",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`symbol` TEXT NOT NULL, `value` REAL NOT NULL, `lastUpdate` INTEGER NOT NULL, PRIMARY KEY(`symbol`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "symbol",
|
||||
"columnName": "symbol",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastUpdate",
|
||||
"columnName": "lastUpdate",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"symbol"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "Icons",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `packageName` TEXT, `activityName` TEXT, `drawable` TEXT, `extras` TEXT, `iconPack` TEXT NOT NULL, `name` TEXT, `themed` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "packageName",
|
||||
"columnName": "packageName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "activityName",
|
||||
"columnName": "activityName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "drawable",
|
||||
"columnName": "drawable",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "extras",
|
||||
"columnName": "extras",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "iconPack",
|
||||
"columnName": "iconPack",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "themed",
|
||||
"columnName": "themed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "IconPack",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `packageName` TEXT NOT NULL, `version` TEXT NOT NULL, `scale` REAL NOT NULL, `themed` INTEGER NOT NULL, PRIMARY KEY(`packageName`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "packageName",
|
||||
"columnName": "packageName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "version",
|
||||
"columnName": "version",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "scale",
|
||||
"columnName": "scale",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "themed",
|
||||
"columnName": "themed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"packageName"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "Widget",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `config` TEXT, `position` INTEGER NOT NULL, `id` BLOB NOT NULL, `parentId` BLOB, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "config",
|
||||
"columnName": "config",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "position",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "parentId",
|
||||
"columnName": "parentId",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "CustomAttributes",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `type` TEXT NOT NULL, `value` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "key",
|
||||
"columnName": "key",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "SearchAction",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`position` INTEGER NOT NULL, `type` TEXT NOT NULL, `data` TEXT, `label` TEXT, `icon` INTEGER, `color` INTEGER, `customIcon` TEXT, `options` TEXT, PRIMARY KEY(`position`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "position",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data",
|
||||
"columnName": "data",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "label",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "icon",
|
||||
"columnName": "icon",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "customIcon",
|
||||
"columnName": "customIcon",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "options",
|
||||
"columnName": "options",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"position"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cb1e4fa1db90a1afddc085714570430d')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -21,10 +21,13 @@ import de.mm20.launcher2.database.migrations.Migration_18_19
|
||||
import de.mm20.launcher2.database.migrations.Migration_19_20
|
||||
import de.mm20.launcher2.database.migrations.Migration_20_21
|
||||
import de.mm20.launcher2.database.migrations.Migration_21_22
|
||||
import de.mm20.launcher2.database.migrations.Migration_22_23
|
||||
import de.mm20.launcher2.database.migrations.Migration_6_7
|
||||
import de.mm20.launcher2.database.migrations.Migration_7_8
|
||||
import de.mm20.launcher2.database.migrations.Migration_8_9
|
||||
import de.mm20.launcher2.database.migrations.Migration_9_10
|
||||
import de.mm20.launcher2.ktx.toBytes
|
||||
import java.util.UUID
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
@ -36,7 +39,7 @@ import de.mm20.launcher2.database.migrations.Migration_9_10
|
||||
WidgetEntity::class,
|
||||
CustomAttributeEntity::class,
|
||||
SearchActionEntity::class
|
||||
], version = 22, exportSchema = true
|
||||
], version = 23, exportSchema = true
|
||||
)
|
||||
@TypeConverters(ComponentNameConverter::class, StringListConverter::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
@ -81,10 +84,15 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
)
|
||||
|
||||
db.execSQL(
|
||||
"INSERT INTO Widget (type, data, height, position, label) VALUES " +
|
||||
"('internal', 'weather', -1, 0, '${context.getString(R.string.widget_name_weather)}')," +
|
||||
"('internal', 'music', -1, 1, '${context.getString(R.string.widget_name_music)}')," +
|
||||
"('internal', 'calendar', -1, 2, '${context.getString(R.string.widget_name_calendar)}');"
|
||||
"INSERT INTO Widget (`type`, `position`, `id`) VALUES " +
|
||||
"('weather', 0, ?)," +
|
||||
"('music', 1, ?)," +
|
||||
"('calendar', 2, ?);",
|
||||
arrayOf(
|
||||
UUID.randomUUID().toBytes(),
|
||||
UUID.randomUUID().toBytes(),
|
||||
UUID.randomUUID().toBytes()
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
@ -104,7 +112,8 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
Migration_18_19(),
|
||||
Migration_19_20(),
|
||||
Migration_20_21(),
|
||||
Migration_21_22()
|
||||
Migration_21_22(),
|
||||
Migration_22_23(),
|
||||
).build()
|
||||
if (_instance == null) _instance = instance
|
||||
return instance
|
||||
|
||||
@ -1,42 +1,54 @@
|
||||
package de.mm20.launcher2.database
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import de.mm20.launcher2.database.entities.WidgetEntity
|
||||
import de.mm20.launcher2.database.entities.PartialWidgetEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.util.UUID
|
||||
|
||||
@Dao
|
||||
interface WidgetDao {
|
||||
@Query("SELECT * FROM Widget ORDER BY position ASC")
|
||||
fun getWidgets(): Flow<List<WidgetEntity>>
|
||||
@Query("SELECT * FROM Widget WHERE parentId IS NULL ORDER BY position ASC LIMIT :limit OFFSET :offset")
|
||||
fun queryRoot(limit: Int, offset: Int): Flow<List<WidgetEntity>>
|
||||
|
||||
@Transaction
|
||||
fun updateWidgets(widgets: List<WidgetEntity>) {
|
||||
deleteAll()
|
||||
insertAll(widgets)
|
||||
}
|
||||
@Query("SELECT * FROM Widget WHERE parentId = :parentId ORDER BY position ASC LIMIT :limit OFFSET :offset")
|
||||
fun queryByParent(parentId: UUID,limit: Int, offset: Int): Flow<List<WidgetEntity>>
|
||||
|
||||
@Insert
|
||||
fun insertAll(widgets: List<WidgetEntity>)
|
||||
suspend fun insert(widget: WidgetEntity)
|
||||
|
||||
@Insert
|
||||
fun insert(widget: WidgetEntity)
|
||||
suspend fun insert(widgets: List<WidgetEntity>)
|
||||
|
||||
@Query("DELETE FROM Widget")
|
||||
fun deleteAll()
|
||||
@Update(entity = WidgetEntity::class)
|
||||
suspend fun patch(widget: PartialWidgetEntity)
|
||||
|
||||
@Update(entity = WidgetEntity::class)
|
||||
suspend fun patch(widgets: List<PartialWidgetEntity>)
|
||||
|
||||
@Query("DELETE FROM Widget WHERE data = :data AND type = :type")
|
||||
fun deleteWidget(type: String, data: String)
|
||||
@Update
|
||||
suspend fun update(widget: WidgetEntity)
|
||||
|
||||
@Query("UPDATE Widget SET height = :newHeight WHERE data = :data AND type = :type")
|
||||
fun updateHeight(type: String, data: String, newHeight: Int)
|
||||
@Update
|
||||
suspend fun update(widgets: List<WidgetEntity>)
|
||||
|
||||
@Query("SELECT EXISTS(SELECT 1 FROM Widget WHERE type = :type AND data = :data)")
|
||||
fun exists(type: String, data: String) : Flow<Boolean>
|
||||
@Query("DELETE FROM Widget WHERE id = :id")
|
||||
suspend fun delete(id: UUID)
|
||||
|
||||
@Query("DELETE FROM WIDGET WHERE id IN (:ids)")
|
||||
suspend fun delete(ids: List<UUID>)
|
||||
|
||||
@Query("DELETE FROM Widget WHERE parentId = :parentId")
|
||||
suspend fun deleteByParent(parentId: UUID)
|
||||
|
||||
@Query("DELETE FROM Widget WHERE parentId IS NULL")
|
||||
suspend fun deleteRoot()
|
||||
|
||||
@Query("SELECT EXISTS(SELECT 1 FROM Widget WHERE type = :type)")
|
||||
fun exists(type: String): Flow<Boolean>
|
||||
|
||||
@Query("SELECT * FROM Widget ORDER BY position ASC LIMIT 1")
|
||||
fun getFirst() : Flow<WidgetEntity?>
|
||||
}
|
||||
@ -2,14 +2,23 @@ package de.mm20.launcher2.database.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import java.util.UUID
|
||||
|
||||
|
||||
@Entity(tableName = "Widget")
|
||||
data class WidgetEntity(
|
||||
val type: String,
|
||||
var data: String,
|
||||
var height: Int,
|
||||
var config: String?,
|
||||
var position: Int,
|
||||
val label: String = "",
|
||||
@PrimaryKey(autoGenerate = true) val id: Int? = null
|
||||
@PrimaryKey val id: UUID,
|
||||
val parentId: UUID? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
* Partial entity for updating and deleting
|
||||
*/
|
||||
data class PartialWidgetEntity(
|
||||
val type: String,
|
||||
var config: String?,
|
||||
@PrimaryKey val id: UUID,
|
||||
)
|
||||
@ -0,0 +1,44 @@
|
||||
package de.mm20.launcher2.database.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import de.mm20.launcher2.ktx.toBytes
|
||||
import org.koin.core.component.KoinComponent
|
||||
import java.util.UUID
|
||||
|
||||
class Migration_22_23 : Migration(22, 23), KoinComponent {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE Widget RENAME TO Widget_old")
|
||||
database.execSQL(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS `Widget` (
|
||||
`type` TEXT NOT NULL,
|
||||
`config` TEXT,
|
||||
`position` INTEGER NOT NULL,
|
||||
`id` BLOB NOT NULL,
|
||||
`parentId` BLOB,
|
||||
PRIMARY KEY(`id`)
|
||||
)
|
||||
"""
|
||||
)
|
||||
val oldWidgets =
|
||||
database.query("SELECT `type`, `data`, `height`, `position` FROM `Widget_old`")
|
||||
while (oldWidgets.moveToNext()) {
|
||||
val oldType = oldWidgets.getString(0)
|
||||
val data = oldWidgets.getString(1)
|
||||
val newType = if (oldType == "3rdparty") "app" else data
|
||||
val height = oldWidgets.getInt(2)
|
||||
val position = oldWidgets.getInt(3)
|
||||
val id = UUID.randomUUID()
|
||||
val config = if (oldType == "3rdparty") {
|
||||
"{\"widgetId\": $data, \"height\": $height}"
|
||||
} else null
|
||||
database.execSQL(
|
||||
"INSERT INTO `Widget` (`type`, `config`, `position`, `id`) VALUES (?, ?, ?, ?)",
|
||||
arrayOf(newType, config, position, id.toBytes())
|
||||
)
|
||||
}
|
||||
oldWidgets.close()
|
||||
database.execSQL("DROP TABLE Widget_old")
|
||||
}
|
||||
}
|
||||
12
core/ktx/src/main/java/de/mm20/launcher2/ktx/UUID.kt
Normal file
12
core/ktx/src/main/java/de/mm20/launcher2/ktx/UUID.kt
Normal file
@ -0,0 +1,12 @@
|
||||
package de.mm20.launcher2.ktx
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.UUID
|
||||
|
||||
fun UUID.toBytes(): ByteArray {
|
||||
val bytes = ByteArray(16)
|
||||
val buffer = ByteBuffer.wrap(bytes)
|
||||
buffer.putLong(mostSignificantBits)
|
||||
buffer.putLong(leastSignificantBits)
|
||||
return buffer.array()
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
kotlin("plugin.serialization")
|
||||
}
|
||||
|
||||
android {
|
||||
@ -42,6 +43,8 @@ dependencies {
|
||||
|
||||
implementation(libs.bundles.androidx.lifecycle)
|
||||
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
|
||||
implementation(project(":data:weather"))
|
||||
|
||||
@ -0,0 +1,56 @@
|
||||
package de.mm20.launcher2.widgets
|
||||
|
||||
import android.app.Activity
|
||||
import android.appwidget.AppWidgetHost
|
||||
import android.appwidget.AppWidgetProviderInfo
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import de.mm20.launcher2.database.entities.PartialWidgetEntity
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class AppWidgetConfig(
|
||||
val widgetId: Int,
|
||||
val height: Int,
|
||||
)
|
||||
|
||||
data class AppWidget(
|
||||
override val id: UUID,
|
||||
val config: AppWidgetConfig,
|
||||
val widgetProviderInfo: AppWidgetProviderInfo
|
||||
) : Widget() {
|
||||
override fun loadLabel(context: Context): String {
|
||||
return widgetProviderInfo.loadLabel(context.packageManager)
|
||||
}
|
||||
|
||||
override fun toDatabaseEntity(): PartialWidgetEntity {
|
||||
return PartialWidgetEntity(
|
||||
id = id,
|
||||
type = Type,
|
||||
config = Json.encodeToString(config),
|
||||
)
|
||||
}
|
||||
|
||||
override val isConfigurable: Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
widgetProviderInfo.widgetFeatures and AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE != 0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
override fun configure(context: Activity, appWidgetHost: AppWidgetHost) {
|
||||
appWidgetHost.startAppWidgetConfigureActivityForResult(
|
||||
context,
|
||||
config.widgetId,
|
||||
0,
|
||||
0,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val Type = "app"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
package de.mm20.launcher2.widgets
|
||||
|
||||
import android.app.Activity
|
||||
import android.appwidget.AppWidgetHost
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import de.mm20.launcher2.database.entities.PartialWidgetEntity
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class CalendarWidgetConfig(
|
||||
val allDayEvents: Boolean = true,
|
||||
val excludedCalendarIds: List<Long> = emptyList(),
|
||||
)
|
||||
data class CalendarWidget(
|
||||
override val id: UUID,
|
||||
val config: CalendarWidgetConfig = CalendarWidgetConfig(),
|
||||
) : Widget() {
|
||||
override fun loadLabel(context: Context): String {
|
||||
return context.getString(R.string.widget_name_calendar)
|
||||
}
|
||||
|
||||
override fun toDatabaseEntity(): PartialWidgetEntity {
|
||||
return PartialWidgetEntity(
|
||||
id = id,
|
||||
type = Type,
|
||||
config = null,
|
||||
)
|
||||
}
|
||||
|
||||
override val isConfigurable: Boolean = true
|
||||
|
||||
override fun configure(context: Activity, appWidgetHost: AppWidgetHost) {
|
||||
val intent = Intent()
|
||||
intent.component = ComponentName(
|
||||
context.getPackageName(),
|
||||
"de.mm20.launcher2.ui.settings.SettingsActivity"
|
||||
)
|
||||
intent.putExtra(
|
||||
"de.mm20.launcher2.settings.ROUTE",
|
||||
"settings/widgets/calendar"
|
||||
)
|
||||
context.tryStartActivity(intent)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val Type = "calendar"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
package de.mm20.launcher2.widgets
|
||||
|
||||
import android.app.Activity
|
||||
import android.appwidget.AppWidgetHost
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import de.mm20.launcher2.database.entities.PartialWidgetEntity
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.util.UUID
|
||||
|
||||
|
||||
@Serializable
|
||||
data class WeatherWidgetConfig(
|
||||
val showForecast: Boolean = true,
|
||||
)
|
||||
|
||||
data class WeatherWidget(
|
||||
override val id: UUID,
|
||||
val config: WeatherWidgetConfig = WeatherWidgetConfig(),
|
||||
) : Widget() {
|
||||
override fun loadLabel(context: Context): String {
|
||||
return context.getString(R.string.widget_name_weather)
|
||||
}
|
||||
|
||||
override fun toDatabaseEntity(): PartialWidgetEntity {
|
||||
return PartialWidgetEntity(
|
||||
id = id,
|
||||
type = Type,
|
||||
config = Json.encodeToString(config),
|
||||
)
|
||||
}
|
||||
|
||||
override val isConfigurable: Boolean = true
|
||||
|
||||
override fun configure(context: Activity, appWidgetHost: AppWidgetHost) {
|
||||
val intent = Intent()
|
||||
intent.component = ComponentName(
|
||||
context.getPackageName(),
|
||||
"de.mm20.launcher2.ui.settings.SettingsActivity"
|
||||
)
|
||||
intent.putExtra(
|
||||
"de.mm20.launcher2.settings.ROUTE",
|
||||
"settings/widgets/weather"
|
||||
)
|
||||
context.tryStartActivity(intent)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val Type = "weather"
|
||||
}
|
||||
}
|
||||
@ -3,86 +3,78 @@ package de.mm20.launcher2.widgets
|
||||
import android.app.Activity
|
||||
import android.appwidget.AppWidgetHost
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProviderInfo
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import de.mm20.launcher2.database.entities.WidgetEntity
|
||||
import de.mm20.launcher2.database.entities.PartialWidgetEntity
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.util.UUID
|
||||
|
||||
sealed class Widget {
|
||||
|
||||
abstract val id: UUID
|
||||
abstract fun loadLabel(context: Context): String
|
||||
abstract fun toDatabaseEntity(position: Int = -1): WidgetEntity
|
||||
fun toDatabaseEntity(position: Int, parentId: UUID? = null): WidgetEntity {
|
||||
return toDatabaseEntity().let {
|
||||
WidgetEntity(
|
||||
id = it.id,
|
||||
type = it.type,
|
||||
config = it.config,
|
||||
position = position,
|
||||
parentId = parentId,
|
||||
)
|
||||
}
|
||||
}
|
||||
abstract fun toDatabaseEntity(): PartialWidgetEntity
|
||||
|
||||
open val isConfigurable: Boolean = false
|
||||
open fun configure(context: Activity, appWidgetHost: AppWidgetHost) {}
|
||||
|
||||
companion object {
|
||||
fun fromDatabaseEntity(context: Context, entity: WidgetEntity): Widget? {
|
||||
if (entity.type == WidgetType.INTERNAL.value) {
|
||||
return when (entity.data) {
|
||||
"weather" -> WeatherWidget
|
||||
"music" -> MusicWidget
|
||||
"calendar" -> CalendarWidget
|
||||
"favorites" -> FavoritesWidget
|
||||
else -> null
|
||||
return when (entity.type) {
|
||||
WeatherWidget.Type -> {
|
||||
val config: WeatherWidgetConfig = Json.decodeFromString(entity.config ?: "{}")
|
||||
WeatherWidget(entity.id, config)
|
||||
}
|
||||
} else {
|
||||
val widgetId = entity.data.toIntOrNull() ?: return null
|
||||
val widgetInfo =
|
||||
AppWidgetManager.getInstance(context).getAppWidgetInfo(widgetId) ?: return null
|
||||
return ExternalWidget(
|
||||
height = entity.height,
|
||||
widgetId = widgetId,
|
||||
widgetProviderInfo = widgetInfo
|
||||
)
|
||||
MusicWidget.Type -> MusicWidget(entity.id)
|
||||
CalendarWidget.Type -> {
|
||||
val config: CalendarWidgetConfig = Json.decodeFromString(entity.config ?: "{}")
|
||||
CalendarWidget(entity.id, config)
|
||||
}
|
||||
FavoritesWidget.Type -> FavoritesWidget(entity.id)
|
||||
AppWidget.Type -> {
|
||||
val config: AppWidgetConfig = Json.decodeFromString(entity.config ?: "{}")
|
||||
AppWidget(
|
||||
entity.id,
|
||||
config,
|
||||
widgetProviderInfo = AppWidgetManager.getInstance(context)
|
||||
.getAppWidgetInfo(config.widgetId)
|
||||
)
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
object WeatherWidget : Widget() {
|
||||
override fun loadLabel(context: Context): String {
|
||||
return context.getString(R.string.widget_name_weather)
|
||||
}
|
||||
|
||||
override fun toDatabaseEntity(position: Int): WidgetEntity {
|
||||
return WidgetEntity(
|
||||
type = WidgetType.INTERNAL.value,
|
||||
data = "weather",
|
||||
height = -1,
|
||||
position = position
|
||||
)
|
||||
}
|
||||
|
||||
override val isConfigurable: Boolean = true
|
||||
|
||||
override fun configure(context: Activity, appWidgetHost: AppWidgetHost) {
|
||||
val intent = Intent()
|
||||
intent.component = ComponentName(
|
||||
context.getPackageName(),
|
||||
"de.mm20.launcher2.ui.settings.SettingsActivity"
|
||||
)
|
||||
intent.putExtra(
|
||||
"de.mm20.launcher2.settings.ROUTE",
|
||||
"settings/widgets/weather"
|
||||
)
|
||||
context.tryStartActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
object MusicWidget : Widget() {
|
||||
data class MusicWidget(
|
||||
override val id: UUID,
|
||||
) : Widget() {
|
||||
override fun loadLabel(context: Context): String {
|
||||
return context.getString(R.string.widget_name_music)
|
||||
}
|
||||
|
||||
override fun toDatabaseEntity(position: Int): WidgetEntity {
|
||||
return WidgetEntity(
|
||||
type = WidgetType.INTERNAL.value,
|
||||
data = "music",
|
||||
height = -1,
|
||||
position = position
|
||||
override fun toDatabaseEntity(): PartialWidgetEntity {
|
||||
return PartialWidgetEntity(
|
||||
id = id,
|
||||
type = Type,
|
||||
config = null
|
||||
)
|
||||
}
|
||||
|
||||
@ -100,50 +92,27 @@ object MusicWidget : Widget() {
|
||||
)
|
||||
context.tryStartActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
object CalendarWidget : Widget() {
|
||||
override fun loadLabel(context: Context): String {
|
||||
return context.getString(R.string.widget_name_calendar)
|
||||
}
|
||||
|
||||
override fun toDatabaseEntity(position: Int): WidgetEntity {
|
||||
return WidgetEntity(
|
||||
type = WidgetType.INTERNAL.value,
|
||||
data = "calendar",
|
||||
height = -1,
|
||||
position = position
|
||||
)
|
||||
}
|
||||
|
||||
override val isConfigurable: Boolean = true
|
||||
|
||||
override fun configure(context: Activity, appWidgetHost: AppWidgetHost) {
|
||||
val intent = Intent()
|
||||
intent.component = ComponentName(
|
||||
context.getPackageName(),
|
||||
"de.mm20.launcher2.ui.settings.SettingsActivity"
|
||||
)
|
||||
intent.putExtra(
|
||||
"de.mm20.launcher2.settings.ROUTE",
|
||||
"settings/widgets/calendar"
|
||||
)
|
||||
context.tryStartActivity(intent)
|
||||
companion object {
|
||||
const val Type = "music"
|
||||
}
|
||||
}
|
||||
|
||||
object FavoritesWidget : Widget() {
|
||||
|
||||
|
||||
|
||||
data class FavoritesWidget(
|
||||
override val id: UUID,
|
||||
) : Widget() {
|
||||
override fun loadLabel(context: Context): String {
|
||||
return context.getString(R.string.widget_name_favorites)
|
||||
}
|
||||
|
||||
override fun toDatabaseEntity(position: Int): WidgetEntity {
|
||||
return WidgetEntity(
|
||||
type = WidgetType.INTERNAL.value,
|
||||
data = "favorites",
|
||||
height = -1,
|
||||
position = position
|
||||
override fun toDatabaseEntity(): PartialWidgetEntity {
|
||||
return PartialWidgetEntity(
|
||||
id = id,
|
||||
type = Type,
|
||||
config = null,
|
||||
)
|
||||
}
|
||||
|
||||
@ -161,44 +130,13 @@ object FavoritesWidget : Widget() {
|
||||
)
|
||||
context.tryStartActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ExternalWidget(
|
||||
var height: Int,
|
||||
val widgetId: Int,
|
||||
val widgetProviderInfo: AppWidgetProviderInfo
|
||||
) : Widget() {
|
||||
override fun loadLabel(context: Context): String {
|
||||
return widgetProviderInfo.loadLabel(context.packageManager)
|
||||
}
|
||||
|
||||
override fun toDatabaseEntity(position: Int): WidgetEntity {
|
||||
return WidgetEntity(
|
||||
type = WidgetType.THIRD_PARTY.value,
|
||||
data = widgetId.toString(),
|
||||
height = height,
|
||||
position = position
|
||||
)
|
||||
}
|
||||
|
||||
override val isConfigurable: Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
widgetProviderInfo.widgetFeatures and AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE != 0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
override fun configure(context: Activity, appWidgetHost: AppWidgetHost) {
|
||||
appWidgetHost.startAppWidgetConfigureActivityForResult(
|
||||
context,
|
||||
widgetId,
|
||||
0,
|
||||
0,
|
||||
null
|
||||
)
|
||||
companion object {
|
||||
const val Type = "favorites"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum class WidgetType(val value: String) {
|
||||
INTERNAL("internal"),
|
||||
THIRD_PARTY("3rdparty")
|
||||
|
||||
@ -1,11 +1,7 @@
|
||||
package de.mm20.launcher2.widgets
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProviderInfo
|
||||
import android.content.Context
|
||||
import android.content.pm.LauncherApps
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Log
|
||||
import androidx.room.withTransaction
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.database.AppDatabase
|
||||
import de.mm20.launcher2.database.entities.WidgetEntity
|
||||
@ -16,22 +12,16 @@ import kotlinx.coroutines.flow.map
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
interface WidgetRepository {
|
||||
fun getWidgets(): Flow<List<Widget>>
|
||||
fun getInternalWidgets(): List<Widget>
|
||||
fun get(parent: UUID? = null, limit: Int = 100, offset: Int = 0): Flow<List<Widget>>
|
||||
fun update(widget: Widget)
|
||||
fun create(widget: Widget, position: Int, parentId: UUID? = null)
|
||||
fun delete(widget: Widget)
|
||||
fun set(widgets: List<Widget>, parentId: UUID? = null)
|
||||
|
||||
suspend fun getAppWidgets(): List<AppWidgetProviderInfo>
|
||||
fun saveWidgets(widgets: List<Widget>)
|
||||
fun addWidget(widget: Widget, position: Int)
|
||||
fun removeWidget(widget: Widget)
|
||||
fun setWidgetHeight(widget: Widget, newHeight: Int)
|
||||
fun isWeatherWidgetEnabled(): Flow<Boolean>
|
||||
fun isMusicWidgetEnabled(): Flow<Boolean>
|
||||
fun isCalendarWidgetEnabled(): Flow<Boolean>
|
||||
fun isFavoritesWidgetEnabled(): Flow<Boolean>
|
||||
|
||||
fun isFavoritesWidgetFirst(): Flow<Boolean>
|
||||
fun exists(type: String): Flow<Boolean>
|
||||
|
||||
suspend fun export(toDir: File)
|
||||
suspend fun import(fromDir: File)
|
||||
@ -43,92 +33,60 @@ internal class WidgetRepositoryImpl(
|
||||
) : WidgetRepository {
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
|
||||
override fun getWidgets(): Flow<List<Widget>> {
|
||||
return database.widgetDao()
|
||||
.getWidgets()
|
||||
.map { it.mapNotNull { Widget.fromDatabaseEntity(context, it) } }
|
||||
}
|
||||
|
||||
override fun getInternalWidgets(): List<Widget> {
|
||||
return listOf(WeatherWidget, MusicWidget, CalendarWidget, FavoritesWidget)
|
||||
}
|
||||
|
||||
override suspend fun getAppWidgets(): List<AppWidgetProviderInfo> {
|
||||
val appWidgetManager = AppWidgetManager.getInstance(context)
|
||||
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||
val profiles = launcherApps.profiles
|
||||
val widgets = mutableListOf<AppWidgetProviderInfo>()
|
||||
withContext(Dispatchers.IO) {
|
||||
for (profile in profiles) {
|
||||
widgets.addAll(appWidgetManager.getInstalledProvidersForProfile(profile))
|
||||
}
|
||||
override fun get(parent: UUID?, limit: Int, offset: Int): Flow<List<Widget>> {
|
||||
val dao = database.widgetDao()
|
||||
return if (parent == null) {
|
||||
dao.queryRoot(limit, offset)
|
||||
} else {
|
||||
dao.queryByParent(parent, limit, offset)
|
||||
}.map {
|
||||
it.mapNotNull { Widget.fromDatabaseEntity(context, it) }
|
||||
}
|
||||
return widgets
|
||||
}
|
||||
|
||||
override fun saveWidgets(widgets: List<Widget>) {
|
||||
override fun update(widget: Widget) {
|
||||
val dao = database.widgetDao()
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
database.widgetDao()
|
||||
.updateWidgets(widgets.mapIndexed { i, widget -> widget.toDatabaseEntity(i) })
|
||||
}
|
||||
dao.patch(widget.toDatabaseEntity())
|
||||
}
|
||||
}
|
||||
|
||||
override fun addWidget(widget: Widget, position: Int) {
|
||||
override fun create(widget: Widget, position: Int, parentId: UUID?) {
|
||||
val dao = database.widgetDao()
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
database.widgetDao()
|
||||
.insert(widget.toDatabaseEntity(position))
|
||||
}
|
||||
val entity = widget.toDatabaseEntity(position = position, parentId = parentId)
|
||||
dao.insert(entity)
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeWidget(widget: Widget) {
|
||||
override fun delete(widget: Widget) {
|
||||
val dao = database.widgetDao()
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val ent = widget.toDatabaseEntity()
|
||||
database.widgetDao().deleteWidget(
|
||||
ent.type,
|
||||
ent.data
|
||||
)
|
||||
}
|
||||
dao.delete(widget.id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setWidgetHeight(widget: Widget, newHeight: Int) {
|
||||
override fun set(widgets: List<Widget>, parentId: UUID?) {
|
||||
val dao = database.widgetDao()
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val ent = widget.toDatabaseEntity()
|
||||
database.widgetDao().updateHeight(
|
||||
ent.type,
|
||||
ent.data,
|
||||
newHeight
|
||||
)
|
||||
database.withTransaction {
|
||||
if (parentId == null) {
|
||||
dao.deleteRoot()
|
||||
} else {
|
||||
dao.deleteByParent(parentId)
|
||||
}
|
||||
dao.insert(widgets.mapIndexed { index, widget ->
|
||||
widget.toDatabaseEntity(position = index, parentId = parentId)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun isWeatherWidgetEnabled(): Flow<Boolean> {
|
||||
return database.widgetDao().exists("internal", "weather")
|
||||
override fun exists(type: String): Flow<Boolean> {
|
||||
val dao = database.widgetDao()
|
||||
return dao.exists(type = type)
|
||||
}
|
||||
|
||||
override fun isMusicWidgetEnabled(): Flow<Boolean> {
|
||||
return database.widgetDao().exists("internal", "music")
|
||||
}
|
||||
|
||||
override fun isCalendarWidgetEnabled(): Flow<Boolean> {
|
||||
return database.widgetDao().exists("internal", "calendar")
|
||||
}
|
||||
|
||||
override fun isFavoritesWidgetEnabled(): Flow<Boolean> {
|
||||
return database.widgetDao().exists("internal", "favorites")
|
||||
}
|
||||
|
||||
override fun isFavoritesWidgetFirst(): Flow<Boolean> {
|
||||
return database.widgetDao().getFirst().map { it?.type == "internal" && it.data == "favorites" }
|
||||
}
|
||||
|
||||
override suspend fun export(toDir: File) = withContext(Dispatchers.IO) {
|
||||
val dao = database.backupDao()
|
||||
@ -140,13 +98,16 @@ internal class WidgetRepositoryImpl(
|
||||
if (widget.type != WidgetType.INTERNAL.value) continue
|
||||
jsonArray.put(
|
||||
jsonObjectOf(
|
||||
"data" to widget.data,
|
||||
"config" to widget.config,
|
||||
"position" to widget.position,
|
||||
"type" to widget.type,
|
||||
"id" to widget.id.toString(),
|
||||
"parentId" to widget.parentId?.toString(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val file = File(toDir, "widgets.${page.toString().padStart(4, '0')}")
|
||||
val file = File(toDir, "widgets2.${page.toString().padStart(4, '0')}")
|
||||
file.bufferedWriter().use {
|
||||
it.write(jsonArray.toString())
|
||||
}
|
||||
@ -158,7 +119,8 @@ internal class WidgetRepositoryImpl(
|
||||
val dao = database.backupDao()
|
||||
dao.wipeWidgets()
|
||||
|
||||
val files = fromDir.listFiles { _, name -> name.startsWith("widgets.") } ?: return@withContext
|
||||
val files =
|
||||
fromDir.listFiles { _, name -> name.startsWith("widgets2.") } ?: return@withContext
|
||||
|
||||
for (file in files) {
|
||||
val widgets = mutableListOf<WidgetEntity>()
|
||||
@ -168,10 +130,11 @@ internal class WidgetRepositoryImpl(
|
||||
for (i in 0 until jsonArray.length()) {
|
||||
val json = jsonArray.getJSONObject(i)
|
||||
val entity = WidgetEntity(
|
||||
type = WidgetType.INTERNAL.value,
|
||||
type = json.getString("type"),
|
||||
position = json.getInt("position"),
|
||||
data = json.getString("data"),
|
||||
height = -1,
|
||||
config = json.optString("config"),
|
||||
id = json.getString("id").let { UUID.fromString(it) },
|
||||
parentId = json.optString("parentId").let { if (it.isEmpty()) null else UUID.fromString(it) }
|
||||
)
|
||||
widgets.add(entity)
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ package de.mm20.launcher2.backup
|
||||
enum class BackupComponent(val value: String) {
|
||||
Settings("settings"),
|
||||
Favorites("favorites"),
|
||||
Widgets("widgets"),
|
||||
Widgets("widgets2"),
|
||||
Customizations("customizations"),
|
||||
SearchActions("searchactions");
|
||||
|
||||
|
||||
@ -188,7 +188,7 @@ class BackupManager(
|
||||
*/
|
||||
|
||||
private const val BackupFormatMajor = 1
|
||||
private const val BackupFormatMinor = 5
|
||||
private const val BackupFormatMinor = 6
|
||||
internal const val BackupFormat = "$BackupFormatMajor.$BackupFormatMinor"
|
||||
}
|
||||
}
|
||||
|
||||
1
services/widgets/.gitignore
vendored
Normal file
1
services/widgets/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
||||
46
services/widgets/build.gradle.kts
Normal file
46
services/widgets/build.gradle.kts
Normal file
@ -0,0 +1,46 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = sdk.versions.compileSdk.get().toInt()
|
||||
|
||||
defaultConfig {
|
||||
minSdk = sdk.versions.minSdk.get().toInt()
|
||||
targetSdk = sdk.versions.targetSdk.get().toInt()
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
namespace = "de.mm20.launcher2.services.widgets"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.bundles.kotlin)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.koin.android)
|
||||
|
||||
implementation(project(":core:base"))
|
||||
implementation(project(":core:i18n"))
|
||||
implementation(project(":data:widgets"))
|
||||
|
||||
}
|
||||
0
services/widgets/consumer-rules.pro
Normal file
0
services/widgets/consumer-rules.pro
Normal file
21
services/widgets/proguard-rules.pro
vendored
Normal file
21
services/widgets/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
4
services/widgets/src/main/AndroidManifest.xml
Normal file
4
services/widgets/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
@ -0,0 +1,7 @@
|
||||
package de.mm20.launcher2.services.widgets
|
||||
|
||||
|
||||
data class BuiltInWidgetInfo(
|
||||
val type: String,
|
||||
val label: String,
|
||||
)
|
||||
@ -0,0 +1,9 @@
|
||||
package de.mm20.launcher2.services.widgets
|
||||
|
||||
import WidgetsService
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
|
||||
val widgetsServiceModule = module {
|
||||
single { WidgetsService(androidContext(), get()) }
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProviderInfo
|
||||
import android.content.Context
|
||||
import android.content.pm.LauncherApps
|
||||
import androidx.core.content.getSystemService
|
||||
import de.mm20.launcher2.services.widgets.BuiltInWidgetInfo
|
||||
import de.mm20.launcher2.services.widgets.R
|
||||
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.Widget
|
||||
import de.mm20.launcher2.widgets.WidgetRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.UUID
|
||||
|
||||
class WidgetsService(
|
||||
private val context: Context,
|
||||
private val widgetRepository: WidgetRepository,
|
||||
) {
|
||||
suspend fun getAppWidgetProviders(): List<AppWidgetProviderInfo> = withContext(Dispatchers.IO) {
|
||||
val appWidgetManager = AppWidgetManager.getInstance(context)
|
||||
val launcherApps = context.getSystemService<LauncherApps>() ?: return@withContext emptyList()
|
||||
val profiles = launcherApps.profiles
|
||||
val widgets = mutableListOf<AppWidgetProviderInfo>()
|
||||
for (profile in profiles) {
|
||||
widgets.addAll(appWidgetManager.getInstalledProvidersForProfile(profile))
|
||||
}
|
||||
widgets
|
||||
}
|
||||
|
||||
fun getBuiltInWidgets(): List<BuiltInWidgetInfo> {
|
||||
return listOf(
|
||||
BuiltInWidgetInfo(
|
||||
type = WeatherWidget.Type,
|
||||
label = context.getString(R.string.widget_name_weather),
|
||||
),
|
||||
BuiltInWidgetInfo(
|
||||
type = MusicWidget.Type,
|
||||
label = context.getString(R.string.widget_name_music),
|
||||
),
|
||||
BuiltInWidgetInfo(
|
||||
type = CalendarWidget.Type,
|
||||
label = context.getString(R.string.widget_name_calendar),
|
||||
),
|
||||
BuiltInWidgetInfo(
|
||||
type = FavoritesWidget.Type,
|
||||
label = context.getString(R.string.widget_name_favorites),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun addWidget(widget: Widget, position: Int, parentId: UUID? = null) {
|
||||
widgetRepository.create(widget, position, parentId)
|
||||
}
|
||||
|
||||
fun getWidgets() = widgetRepository.get()
|
||||
|
||||
fun isFavoritesWidgetFirst(): Flow<Boolean> {
|
||||
return widgetRepository.get(limit = 1).map {
|
||||
it.firstOrNull() is FavoritesWidget
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val AppWidgetHostId = 44203
|
||||
}
|
||||
}
|
||||
@ -30,6 +30,9 @@ dependencyResolutionManagement {
|
||||
"kotlinx.collections.immutable"
|
||||
)
|
||||
)
|
||||
version("kotlinx.serialization", "1.5.0")
|
||||
library("kotlinx.serialization.json", "org.jetbrains.kotlinx", "kotlinx-serialization-json")
|
||||
.versionRef("kotlinx.serialization")
|
||||
|
||||
version("androidx.compose.compiler", "1.4.4")
|
||||
library("androidx.compose.runtime", "androidx.compose.runtime", "runtime")
|
||||
@ -297,3 +300,4 @@ include(":libs:webdav")
|
||||
include(":libs:g-services")
|
||||
include(":libs:ms-services")
|
||||
include(":services:global-actions")
|
||||
include(":services:widgets")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user