From 956af82f79995a5d6d3747f01e56f9f467f4dabb Mon Sep 17 00:00:00 2001
From: MM20 <15646950+MM2-0@users.noreply.github.com>
Date: Tue, 29 Mar 2022 18:13:31 +0200
Subject: [PATCH] Refactor widgets
---
.../main/res/drawable/anim_ic_edit_add.xml | 45 +-
.../de/mm20/launcher2/database/WidgetDao.kt | 16 +-
.../ui/launcher/LauncherScaffoldView.kt | 2 +-
.../ui/launcher/widgets/WidgetItem.kt | 164 ++++++
.../ui/launcher/widgets/WidgetsVM.kt | 48 +-
.../ui/launcher/widgets/WidgetsView.kt | 488 ++++++++++--------
.../widgets/external/ExternalWidget.kt | 58 +++
.../ui/legacy/component/WidgetView.kt | 120 -----
.../ui/legacy/view/WidgetResizeDragView.kt | 43 --
.../ui/legacy/widget/CalendarWidget.kt | 59 ---
.../launcher2/ui/legacy/widget/ClockWidget.kt | 43 --
.../ui/legacy/widget/ExternalWidget.kt | 75 ---
.../ui/legacy/widget/LauncherWidget.kt | 17 -
.../launcher2/ui/legacy/widget/MusicWidget.kt | 47 --
.../ui/legacy/widget/WeatherWidget.kt | 50 --
.../res/layout/view_launcher_scaffold.xml | 3 +-
ui/src/main/res/layout/view_widget.xml | 80 ---
ui/src/main/res/layout/view_widgets.xml | 34 --
.../java/de/mm20/launcher2/widgets/Module.kt | 2 +-
.../java/de/mm20/launcher2/widgets/Widget.kt | 183 ++++++-
.../launcher2/widgets/WidgetRepository.kt | 73 ++-
21 files changed, 804 insertions(+), 846 deletions(-)
create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetItem.kt
create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/external/ExternalWidget.kt
delete mode 100644 ui/src/main/java/de/mm20/launcher2/ui/legacy/component/WidgetView.kt
delete mode 100644 ui/src/main/java/de/mm20/launcher2/ui/legacy/view/WidgetResizeDragView.kt
delete mode 100644 ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/CalendarWidget.kt
delete mode 100644 ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/ClockWidget.kt
delete mode 100644 ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/ExternalWidget.kt
delete mode 100644 ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/LauncherWidget.kt
delete mode 100644 ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/MusicWidget.kt
delete mode 100644 ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/WeatherWidget.kt
delete mode 100644 ui/src/main/res/layout/view_widget.xml
delete mode 100644 ui/src/main/res/layout/view_widgets.xml
diff --git a/base/src/main/res/drawable/anim_ic_edit_add.xml b/base/src/main/res/drawable/anim_ic_edit_add.xml
index 3329ff69..741278ab 100644
--- a/base/src/main/res/drawable/anim_ic_edit_add.xml
+++ b/base/src/main/res/drawable/anim_ic_edit_add.xml
@@ -8,36 +8,51 @@
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
-
-
+
+
+
+
+
+
+
+
+
diff --git a/database/src/main/java/de/mm20/launcher2/database/WidgetDao.kt b/database/src/main/java/de/mm20/launcher2/database/WidgetDao.kt
index d1584204..1116806d 100644
--- a/database/src/main/java/de/mm20/launcher2/database/WidgetDao.kt
+++ b/database/src/main/java/de/mm20/launcher2/database/WidgetDao.kt
@@ -1,7 +1,9 @@
package de.mm20.launcher2.database
-import androidx.lifecycle.LiveData
-import androidx.room.*
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.Transaction
import de.mm20.launcher2.database.entities.WidgetEntity
import kotlinx.coroutines.flow.Flow
@@ -19,6 +21,16 @@ interface WidgetDao {
@Insert
fun insertAll(widgets: List)
+ @Insert
+ fun insert(widget: WidgetEntity)
+
@Query("DELETE FROM Widget")
fun deleteAll()
+
+
+ @Query("DELETE FROM Widget WHERE data = :data AND type = :type")
+ fun deleteWidget(type: String, data: String)
+
+ @Query("UPDATE Widget SET height = :newHeight WHERE data = :data AND type = :type")
+ fun updateHeight(type: String, data: String, newHeight: Int)
}
\ No newline at end of file
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/LauncherScaffoldView.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/LauncherScaffoldView.kt
index f7439de5..143985b3 100644
--- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/LauncherScaffoldView.kt
+++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/LauncherScaffoldView.kt
@@ -173,7 +173,7 @@ class LauncherScaffoldView @JvmOverloads constructor(
}
widgetsViewModel.isEditMode.observe(context) {
- OneShotLayoutTransition.run(binding.scrollContainer)
+ //OneShotLayoutTransition.run(binding.scrollContainer)
if (it) {
binding.scrollView.setOnTouchListener(null)
binding.searchBar.visibility = View.INVISIBLE
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetItem.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetItem.kt
new file mode 100644
index 00000000..48cb35ba
--- /dev/null
+++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetItem.kt
@@ -0,0 +1,164 @@
+package de.mm20.launcher2.ui.launcher.widgets
+
+import android.app.Activity
+import android.appwidget.AppWidgetHost
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.gestures.*
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.*
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.zIndex
+import de.mm20.launcher2.ui.R
+import de.mm20.launcher2.ui.component.LauncherCard
+import de.mm20.launcher2.ui.launcher.widgets.calendar.CalendarWidget
+import de.mm20.launcher2.ui.launcher.widgets.external.ExternalWidget
+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
+fun WidgetItem(
+ widget: Widget,
+ appWidgetHost: AppWidgetHost,
+ modifier: Modifier = Modifier,
+ editMode: Boolean = false,
+ onWidgetResize: (newHeight: Int) -> Unit = {},
+ onWidgetRemove: () -> Unit = {},
+ draggableState: DraggableState = rememberDraggableState {},
+ onDragStopped: () -> Unit = {}
+) {
+ val context = LocalContext.current
+ var resizeMode by remember(editMode) { mutableStateOf(false) }
+
+ var isDragged by remember { mutableStateOf(false) }
+ val elevation by animateDpAsState(if (isDragged) 8.dp else 2.dp)
+
+ LauncherCard(
+ modifier = modifier.zIndex(if (isDragged) 1f else 0f),
+ elevation = elevation
+ ) {
+ Column {
+ AnimatedVisibility(editMode) {
+ Row(
+ modifier = Modifier.padding(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.DragIndicator,
+ contentDescription = null,
+ modifier = Modifier.draggable(
+ state = draggableState,
+ orientation = Orientation.Vertical,
+ startDragImmediately = true,
+ onDragStarted = {
+ isDragged = true
+ },
+ onDragStopped = {
+ isDragged = false
+ onDragStopped()
+ }
+ )
+ )
+ Text(
+ text = remember(widget) { widget.loadLabel(context) },
+ style = MaterialTheme.typography.titleMedium,
+ modifier = Modifier
+ .weight(1f)
+ .padding(horizontal = 8.dp),
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1
+ )
+ if (widget is ExternalWidget) {
+ IconButton(onClick = { resizeMode = !resizeMode }) {
+ Icon(
+ imageVector = Icons.Rounded.Edit,
+ contentDescription = stringResource(R.string.widget_action_adjust_height)
+ )
+ }
+ }
+ if (widget.isConfigurable) {
+ IconButton({
+ widget.configure(context as Activity, appWidgetHost)
+ }) {
+ Icon(
+ imageVector = Icons.Rounded.Settings,
+ contentDescription = stringResource(R.string.settings)
+ )
+ }
+ }
+ IconButton(onClick = { onWidgetRemove() }) {
+ Icon(
+ imageVector = Icons.Rounded.Delete,
+ contentDescription = stringResource(R.string.widget_action_remove)
+ )
+ }
+ }
+ }
+ AnimatedVisibility(!editMode || resizeMode) {
+ when (widget) {
+ is WeatherWidget -> {
+ WeatherWidget()
+ }
+ is MusicWidget -> {
+ MusicWidget()
+ }
+ is CalendarWidget -> {
+ CalendarWidget()
+ }
+ is ExternalWidget -> {
+ var height by remember(widget) { mutableStateOf(widget.height) }
+ Column {
+ ExternalWidget(
+ appWidgetHost = appWidgetHost,
+ widgetId = widget.widgetId,
+ modifier = Modifier.fillMaxWidth(),
+ height = height,
+ )
+ if (resizeMode) {
+ val density = LocalDensity.current
+ val drgStt = rememberDraggableState {
+ height = max(
+ height + (it / density.density).roundToInt(),
+ widget.widgetProviderInfo.minResizeHeight
+ )
+ }
+ Icon(
+ imageVector = Icons.Rounded.DragHandle,
+ contentDescription = null,
+ modifier = Modifier
+ .padding(top = 12.dp)
+ .requiredHeight(24.dp)
+ .fillMaxWidth()
+ .draggable(
+ state = drgStt,
+ orientation = Orientation.Vertical,
+ startDragImmediately = true,
+ onDragStopped = {
+ onWidgetResize(height)
+ }
+ )
+ )
+ }
+
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsVM.kt
index 1f9db8df..59b1c4f2 100644
--- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsVM.kt
+++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsVM.kt
@@ -1,11 +1,17 @@
package de.mm20.launcher2.ui.launcher.widgets
+import android.appwidget.AppWidgetManager
+import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData
+import de.mm20.launcher2.widgets.ExternalWidget
import de.mm20.launcher2.widgets.Widget
import de.mm20.launcher2.widgets.WidgetRepository
+import de.mm20.launcher2.widgets.WidgetType
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -20,11 +26,47 @@ class WidgetsVM : ViewModel(), KoinComponent {
isEditMode.value = editMode
}
- fun saveWidgets(widgets: List) {
+ 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)
+ }
+
+ fun setWidgetHeight(widget: Widget, newHeight: Int) {
+ widgetRepository.setWidgetHeight(widget, newHeight)
+ }
+
+ fun getAvailableBuiltInWidgets(): List {
+ return widgetRepository.getInternalWidgets().filter {
+ widgets.value?.contains(it)?.not() ?: false
+ }
+ }
+
+ fun moveUp(index: Int) {
+ val widgets = widgets.value?.toMutableList() ?: return
+ val widget = widgets.removeAt(index)
+ widgets.add(index - 1, widget)
widgetRepository.saveWidgets(widgets)
}
- fun getInternalWidgets(): List {
- return widgetRepository.getInternalWidgets()
+ fun moveDown(index: Int) {
+ val widgets = widgets.value?.toMutableList() ?: return
+ val widget = widgets.removeAt(index)
+ widgets.add(index + 1, widget)
+ widgetRepository.saveWidgets(widgets)
}
}
\ No newline at end of file
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsView.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsView.kt
index 519da74c..046c3d58 100644
--- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsView.kt
+++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsView.kt
@@ -1,7 +1,5 @@
package de.mm20.launcher2.ui.launcher.widgets
-import android.animation.LayoutTransition
-import android.annotation.SuppressLint
import android.app.Activity
import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetManager
@@ -9,259 +7,317 @@ import android.content.Context
import android.content.Intent
import android.util.AttributeSet
import android.util.Log
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.LinearLayout
+import android.widget.FrameLayout
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
-import androidx.core.view.iterator
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import com.afollestad.materialdialogs.MaterialDialog
-import com.afollestad.materialdialogs.list.listItems
-import com.balsikandar.crashreporter.CrashReporter
-import de.mm20.launcher2.ktx.dp
-import de.mm20.launcher2.transition.ChangingLayoutTransition
-import de.mm20.launcher2.transition.OneShotLayoutTransition
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
+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.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Add
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.layout.positionInParent
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.lifecycle.MutableLiveData
+import de.mm20.launcher2.ui.ClockWidget
+import de.mm20.launcher2.ui.MdcLauncherTheme
import de.mm20.launcher2.ui.R
-import de.mm20.launcher2.ui.databinding.ViewWidgetsBinding
+import de.mm20.launcher2.ui.base.ProvideSettings
+import de.mm20.launcher2.ui.ktx.toDp
import de.mm20.launcher2.ui.launcher.widgets.picker.PickAppWidgetActivity
-import de.mm20.launcher2.ui.legacy.component.WidgetView
-import de.mm20.launcher2.widgets.Widget
-import de.mm20.launcher2.widgets.WidgetType
-import kotlinx.coroutines.awaitCancellation
+import de.mm20.launcher2.widgets.ExternalWidget
import kotlinx.coroutines.launch
-import java.util.*
-import kotlin.math.roundToInt
+@OptIn(ExperimentalAnimationGraphicsApi::class)
class WidgetsView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
-) : LinearLayout(context, attrs) {
-
- private val binding = ViewWidgetsBinding.inflate(LayoutInflater.from(context), this)
+) : FrameLayout(context, attrs) {
private val widgetHost: AppWidgetHost = AppWidgetHost(context.applicationContext, 44203)
private val viewModel: WidgetsVM by (context as AppCompatActivity).viewModels()
- private lateinit var widgets: MutableList
-
private val pickWidgetLauncher: ActivityResultLauncher
+ private val clockWidgetHeight = MutableLiveData(0)
+
init {
context as AppCompatActivity
-
pickWidgetLauncher = context.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
val data = it.data ?: return@registerForActivityResult
- val widgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
+ val widgetId = data.getIntExtra(
+ AppWidgetManager.EXTRA_APPWIDGET_ID,
+ AppWidgetManager.INVALID_APPWIDGET_ID
+ )
if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return@registerForActivityResult
if (it.resultCode == Activity.RESULT_OK) {
- bindAppWidget(widgetId)
+ viewModel.addAppWidget(context, widgetId)
}
}
- viewModel.widgets.observe(context) {
- if (it != null && !::widgets.isInitialized) {
- widgets = it.toMutableList()
- initWidgets()
- }
- }
- viewModel.isEditMode.observe(context) {
- if (it) {
- OneShotLayoutTransition.run(binding.widgetList)
- binding.clockWidget.visibility = View.GONE
+ val composeView = ComposeView(context)
+ composeView.setContent {
+ MdcLauncherTheme {
+ ProvideSettings {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .padding(8.dp)
+ ) {
+ val editMode by viewModel.isEditMode.observeAsState(false)
+ val clockHeight by clockWidgetHeight.observeAsState(0)
+ val scope = rememberCoroutineScope()
+ var showAddDialog by remember { mutableStateOf(false) }
- for (v in binding.widgetList.iterator()) {
- if (v is WidgetView) {
- v.editMode = true
- v.onResizeModeChange = {
- OneShotLayoutTransition.run(binding.widgetList)
- OneShotLayoutTransition.run(this)
+ AnimatedVisibility(!editMode) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(clockHeight.toDp()),
+ contentAlignment = Alignment.BottomCenter
+ ) {
+ ClockWidget(
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+ val widgets by viewModel.widgets.observeAsState(emptyList())
+ Column {
+ val swapThresholds = remember(widgets) {
+ Array(widgets.size) { floatArrayOf(0f, 0f) }
+ }
+ for ((i, widget) in widgets.withIndex()) {
+ key(if (widget is ExternalWidget) widget.widgetId else widget) {
+ var dragOffsetAfterSwap = remember { null }
+ val offsetY = remember(widgets) { Animatable(dragOffsetAfterSwap ?: 0f) }
+
+ LaunchedEffect(widgets) {
+ dragOffsetAfterSwap = null
+ }
+
+ WidgetItem(
+ widget = widget,
+ appWidgetHost = widgetHost,
+ editMode = editMode,
+ onWidgetRemove = {
+ if (widget is ExternalWidget) {
+ widgetHost.deleteAppWidgetId(widget.widgetId)
+ }
+ viewModel.removeWidget(widget)
+ },
+ onWidgetResize = {
+ viewModel.setWidgetHeight(widget, it)
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .onPlaced {
+ swapThresholds[i][0] = it.positionInParent().y
+ swapThresholds[i][1] = it.positionInParent().y + it.size.height
+ }
+ .padding(top = 8.dp)
+ .offset {
+ IntOffset(0, offsetY.value.toInt())
+ },
+ draggableState = rememberDraggableState {
+ scope.launch {
+ val newOffset = offsetY.value + it
+ offsetY.snapTo(newOffset)
+ if (i > 0 && newOffset < (swapThresholds[i - 1][0] - swapThresholds[i - 1][1])) {
+ if (dragOffsetAfterSwap == null) {
+ dragOffsetAfterSwap = swapThresholds[i - 1][1] - swapThresholds[i - 1][0] + newOffset
+ viewModel.moveUp(i)
+ }
+ }
+ if (i < widgets.lastIndex && newOffset > (swapThresholds[i + 1][1] - swapThresholds[i + 1][0])) {
+ if (dragOffsetAfterSwap == null) {
+ dragOffsetAfterSwap = swapThresholds[i + 1][0] - swapThresholds[i + 1][1] + newOffset
+ viewModel.moveDown(i)
+ }
+ }
+ }
+ },
+ onDragStopped = {
+ scope.launch {
+ offsetY.animateTo(0f)
+ }
+ }
+ )
+ }
+ }
+ }
+
+ val icon =
+ AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_edit_add)
+ ExtendedFloatingActionButton(
+ modifier = Modifier
+ .padding(16.dp)
+ .align(Alignment.CenterHorizontally),
+ icon = {
+ Icon(
+ painter = rememberAnimatedVectorPainter(
+ animatedImageVector = icon,
+ atEnd = !editMode
+ ), contentDescription = null
+ )
+ },
+ text = {
+ Text(
+ stringResource(
+ if (editMode) R.string.widget_add_widget
+ else R.string.menu_edit_widgets
+ )
+ )
+ }, onClick = {
+ if (!editMode) {
+ viewModel.setEditMode(true)
+ } else {
+ if (viewModel.getAvailableBuiltInWidgets().isEmpty()) {
+ pickWidgetLauncher.launch(
+ Intent(
+ context,
+ PickAppWidgetActivity::class.java
+ )
+ )
+ } else {
+ showAddDialog = true
+ }
+ }
+ })
+
+ if (showAddDialog) {
+ val availableBuiltInWidgets =
+ remember { viewModel.getAvailableBuiltInWidgets() }
+ Dialog(onDismissRequest = { showAddDialog = false }) {
+ Surface(
+ tonalElevation = 16.dp,
+ shadowElevation = 16.dp,
+ shape = RoundedCornerShape(16.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp),
+ ) {
+ Column(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(
+ text = stringResource(R.string.widget_add_widget),
+ style = MaterialTheme.typography.titleLarge,
+ modifier = Modifier.padding(
+ start = 24.dp,
+ end = 24.dp,
+ top = 24.dp,
+ bottom = 8.dp
+ )
+ )
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ top = 16.dp
+ )
+ ) {
+ items(availableBuiltInWidgets) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ viewModel.addWidget(it)
+ showAddDialog = false
+ }
+ .padding(
+ horizontal = 24.dp,
+ vertical = 16.dp
+ )
+ ) {
+ Text(
+ text = it.loadLabel(context),
+ style = MaterialTheme.typography.bodyLarge
+ )
+ }
+ }
+ item {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp)
+ .clickable {
+ pickWidgetLauncher.launch(
+ Intent(
+ context,
+ PickAppWidgetActivity::class.java
+ )
+ )
+ }
+ .padding(
+ horizontal = 24.dp,
+ vertical = 16.dp
+ ),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.Add,
+ contentDescription = null,
+ modifier = Modifier.padding(end = 8.dp)
+ )
+ Text(
+ text = stringResource(R.string.widget_add_external),
+ style = MaterialTheme.typography.titleMedium
+ )
+ }
+ }
+ }
+
+ TextButton(
+ onClick = { showAddDialog = false },
+ modifier = Modifier
+ .align(Alignment.End)
+ .padding(bottom = 16.dp, end = 24.dp)
+ ) {
+ Text(
+ stringResource(android.R.string.cancel)
+ )
+ }
+ }
+ }
+ }
}
}
- }
- binding.fabEditWidget.apply {
- setIconResource(R.drawable.ic_add)
- setText(R.string.widget_add_widget)
- setOnClickListener {
- addWidget()
- }
- }
- } else {
- if (::widgets.isInitialized) {
- viewModel.saveWidgets(widgets)
- OneShotLayoutTransition.run(binding.widgetList)
- }
- binding.clockWidget.visibility = View.VISIBLE
- for (v in binding.widgetList.iterator()) {
- if (v is WidgetView) {
- v.editMode = false
- }
- }
- binding.fabEditWidget.apply {
- setIconResource(R.drawable.ic_edit)
- setText(R.string.menu_edit_widgets)
- setOnClickListener {
- viewModel.setEditMode(true)
- }
+
+
}
}
}
-
- context.lifecycleScope.launch {
- context.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
- widgetHost.startListening()
- try {
- awaitCancellation()
- } finally {
- // TODO: find out why there is a NPE thrown sometimes
- try {
- widgetHost.stopListening()
- } catch (e: NullPointerException) {
- CrashReporter.logException(e)
- }
- }
- }
- }
-
- binding.fabEditWidget.setOnClickListener {
- viewModel.setEditMode(true)
- }
+ addView(composeView)
}
fun setClockWidgetHeight(height: Int) {
- val params = binding.clockWidget.layoutParams
- params.height = height
- binding.clockWidget.layoutParams = params
- }
-
-
- private fun initWidgets() {
- binding.widgetList.removeAllViews()
- val params = LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- )
- params.topMargin = (8 * dp).roundToInt()
- for (w in widgets) {
- val view = WidgetView(context)
- view.layoutParams = params
- if (view.setWidget(w, widgetHost)) {
- binding.widgetList.addDragView(view, view.getDragHandle())
- view.onRemove = {
- OneShotLayoutTransition.run(binding.widgetList)
- binding.widgetList.removeDragView(view)
- removeWidget(view.widget)
- }
- view.onResizeModeChange = {
- OneShotLayoutTransition.run(binding.widgetList)
- }
- }
- }
-
- binding.widgetList.setOnViewSwapListener { _, firstPosition, _, secondPosition ->
- Collections.swap(widgets, firstPosition, secondPosition)
- }
- }
-
-
- @SuppressLint("CheckResult")
- private fun addWidget() {
- val usedWidgets = widgets.filter { it.type == WidgetType.INTERNAL }.map { it.data }
- val internalWidgets =
- viewModel.getInternalWidgets().filter { !usedWidgets.contains(it.data) }
- if (internalWidgets.isNotEmpty()) {
- MaterialDialog(context).show {
- title(R.string.widget_add_widget)
- listItems(items = internalWidgets.map { it.label }) { dialog, index, _ ->
- val widget = internalWidgets[index]
- val view = WidgetView(this@WidgetsView.context)
- val params = LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- )
- params.topMargin = (8 * dp).roundToInt()
- view.layoutParams = params
- if (view.setWidget(widget, widgetHost)) {
- view.editMode = true
- binding.widgetList.addDragView(view, view.getDragHandle())
- view.onRemove = {
- OneShotLayoutTransition.run(binding.widgetList)
- OneShotLayoutTransition.run(this@WidgetsView)
- binding.widgetList.removeDragView(view)
- removeWidget(view.widget)
- }
- view.onResizeModeChange = {
- OneShotLayoutTransition.run(binding.widgetList)
- OneShotLayoutTransition.run(this@WidgetsView)
- }
- widgets.add(widget)
- }
- dialog.dismiss()
- }
- @Suppress("DEPRECATION") // I don't care that neutral buttons are discouraged.
- neutralButton(R.string.widget_add_external) {
- pickAppWidget()
- it.dismiss()
- }
- }
- } else {
- pickAppWidget()
- }
- }
-
- private fun removeWidget(widget: Widget?) {
- widget ?: return
- widgets.remove(widget)
- val id = widget.data.toIntOrNull() ?: return
- widgetHost.deleteAppWidgetId(id)
- }
-
- private fun pickAppWidget() {
- val pickIntent = Intent(context, PickAppWidgetActivity::class.java)
- pickWidgetLauncher.launch(pickIntent)
- }
-
- private fun bindAppWidget(widgetId: Int) {
- if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return
- val appWidget = AppWidgetManager.getInstance(context)
- .getAppWidgetInfo(widgetId) ?: return
- val widget = Widget(
- type = WidgetType.THIRD_PARTY,
- data = widgetId.toString(),
- height = appWidget.minHeight
- )
- val params = LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- )
- params.topMargin = (8 * dp).roundToInt()
- val view = WidgetView(context)
- view.layoutParams = params
- if (view.setWidget(widget, widgetHost)) {
- view.editMode = true
-
- binding.widgetList.addDragView(view, view.getDragHandle())
- view.onRemove = {
- OneShotLayoutTransition.run(binding.widgetList)
- OneShotLayoutTransition.run(this)
- binding.widgetList.removeDragView(view)
- removeWidget(view.widget)
- }
- view.onResizeModeChange = {
- OneShotLayoutTransition.run(binding.widgetList)
- OneShotLayoutTransition.run(this)
- }
- widgets.add(widget)
- }
+ clockWidgetHeight.value = height
}
}
\ No newline at end of file
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/external/ExternalWidget.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/external/ExternalWidget.kt
new file mode 100644
index 00000000..a48ce5c8
--- /dev/null
+++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/external/ExternalWidget.kt
@@ -0,0 +1,58 @@
+package de.mm20.launcher2.ui.launcher.widgets.external
+
+import android.appwidget.AppWidgetHost
+import android.appwidget.AppWidgetManager
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ListView
+import android.widget.ScrollView
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.key
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.view.doOnNextLayout
+import androidx.core.view.iterator
+import de.mm20.launcher2.ui.ktx.toPixels
+
+@Composable
+fun ExternalWidget(
+ appWidgetHost: AppWidgetHost,
+ widgetId: Int,
+ height: Int,
+ modifier: Modifier = Modifier
+) {
+ val context = LocalContext.current
+ val widgetInfo = remember(widgetId) {
+ AppWidgetManager.getInstance(context).getAppWidgetInfo(widgetId)
+ }
+ val viewHeightPx = height.dp.toPixels()
+ key(widgetId) {
+ AndroidView(
+ modifier = modifier.fillMaxWidth().height(height.dp),
+ factory = {
+ val view = appWidgetHost.createView(it.applicationContext, widgetId, widgetInfo)
+ enableNestedScroll(view)
+ return@AndroidView view
+ },
+ update = {
+ it.updateAppWidgetSize(null, 0, 0, it.width, height)
+ }
+ )
+ }
+}
+
+private fun enableNestedScroll(view: View) {
+ if (view is ViewGroup) {
+ for (child in view.iterator()) {
+ enableNestedScroll(child)
+ }
+ }
+ if (view is ListView || view is ScrollView) view.isNestedScrollingEnabled = true
+}
\ No newline at end of file
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/WidgetView.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/WidgetView.kt
deleted file mode 100644
index 6b80b3bd..00000000
--- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/WidgetView.kt
+++ /dev/null
@@ -1,120 +0,0 @@
-package de.mm20.launcher2.ui.legacy.component
-
-import android.appwidget.AppWidgetHost
-import android.content.Context
-import android.util.AttributeSet
-import android.view.LayoutInflater
-import android.view.View
-import androidx.appcompat.widget.TooltipCompat
-import androidx.core.view.get
-import de.mm20.launcher2.ktx.dp
-import de.mm20.launcher2.transition.OneShotLayoutTransition
-import de.mm20.launcher2.ui.R
-import de.mm20.launcher2.ui.databinding.ViewWidgetBinding
-import de.mm20.launcher2.ui.legacy.view.LauncherCardView
-import de.mm20.launcher2.ui.legacy.widget.*
-import de.mm20.launcher2.widgets.Widget
-import de.mm20.launcher2.widgets.WidgetType
-
-class WidgetView : LauncherCardView {
-
- var onRemove: (() -> Unit)? = null
-
-
- var widget: Widget? = null
-
- var widgetView: LauncherWidget? = null
-
- var editMode = false
- set(value) {
- OneShotLayoutTransition.run(this)
- if (value) {
- binding.widgetControlPanel.visibility = View.VISIBLE
- val widget = binding.widgetWrapper[2]
- widget.visibility = View.GONE
- binding.widgetName.visibility = View.VISIBLE
- visibility = View.VISIBLE
- } else {
- resizeMode = false
- binding.widgetControlPanel.visibility = View.GONE
- val widget = binding.widgetWrapper[2] as LauncherWidget
- widget.visibility = View.VISIBLE
- binding.widgetName.visibility = View.GONE
- }
- field = value
- }
-
- private var resizeMode = false
- set(value) {
- if (value == field) return
- onResizeModeChange?.invoke(value)
- OneShotLayoutTransition.run(this)
- OneShotLayoutTransition.run(binding.widgetWrapper)
- if (value) {
- binding.widgetResizeDragHandle.visibility = View.VISIBLE
- val widget = binding.widgetWrapper[2]
- widget.visibility = View.VISIBLE
- binding.widgetName.visibility = View.GONE
- } else {
- binding.widgetResizeDragHandle.visibility = View.GONE
- if (editMode) {
- val widget = binding.widgetWrapper[2]
- widget.visibility = View.GONE
- binding.widgetName.visibility = View.VISIBLE
- }
-
- }
- field = value
- }
-
- constructor(context: Context) : super(context)
- constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
- constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
-
- private val binding = ViewWidgetBinding.inflate(LayoutInflater.from(context), this)
-
- init {
- binding.widgetActionResize.setOnClickListener {
- resizeMode = !resizeMode
- }
- binding.widgetActionRemove.setOnClickListener {
- onRemove?.invoke()
- }
-
- TooltipCompat.setTooltipText(binding.widgetActionResize, context.getString(R.string.widget_action_adjust_height))
- TooltipCompat.setTooltipText(binding.widgetActionRemove, context.getString(R.string.widget_action_remove))
- }
-
- var onResizeModeChange: ((Boolean) -> Unit)? = null
-
- fun setWidget(widget: Widget, widgetHost: AppWidgetHost): Boolean {
- if (widget.type == WidgetType.INTERNAL) {
- widgetView = when (widget.data) {
- CalendarWidget.ID -> CalendarWidget(context)
- WeatherWidget.ID -> WeatherWidget(context)
- MusicWidget.ID -> MusicWidget(context)
- else -> return false
- }
- binding.widgetActionResize.visibility = View.GONE
- binding.widgetResizeDragHandle.resizeView = widgetView
- binding.widgetWrapper.addView(widgetView, 2)
- binding.widgetName.text = widgetView?.name
- } else {
- widgetView = ExternalWidget(context, widget, widgetHost)
- binding.widgetResizeDragHandle.resizeView = widgetView
- binding.widgetResizeDragHandle.onResize = {
- widget.height = (it / dp).toInt()
- }
- binding.widgetWrapper.addView(widgetView, 2)
- binding.widgetName.text = widgetView?.name
- binding.widgetActionResize.visibility = View.VISIBLE
- }
- this.widget = widget
- return true
- }
-
- fun getDragHandle(): View {
- return binding.widgetDragHandle
- }
-
-}
\ No newline at end of file
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/view/WidgetResizeDragView.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/view/WidgetResizeDragView.kt
deleted file mode 100644
index 1476e3e3..00000000
--- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/view/WidgetResizeDragView.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-package de.mm20.launcher2.ui.legacy.view
-
-import android.content.Context
-import android.util.AttributeSet
-import android.util.Log
-import android.view.MotionEvent
-import android.view.MotionEvent.INVALID_POINTER_ID
-import android.view.View
-import android.widget.ImageView
-
-class WidgetResizeDragView : ImageView {
-
- var resizeView: View? = null
-
- private var lastY = 0f
-
- var onResize: ((Int) -> Unit)? = null
-
- constructor(context: Context) : super(context)
- constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
- constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
-
- override fun onTouchEvent(event: MotionEvent): Boolean {
- when (event.action) {
- MotionEvent.ACTION_DOWN -> {
- parent.requestDisallowInterceptTouchEvent(true)
- val y = event.y
- lastY = y
- }
- MotionEvent.ACTION_MOVE -> {
- val y = event.y
- val dY = y - lastY
- val view = resizeView ?: return false
- val params = view.layoutParams
- val newHeight = (view.height + dY).toInt()
- params.height = newHeight
- onResize?.invoke(newHeight)
- view.layoutParams = params
- }
- }
- return true
- }
-}
\ No newline at end of file
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/CalendarWidget.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/CalendarWidget.kt
deleted file mode 100644
index 9ed2d494..00000000
--- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/CalendarWidget.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-package de.mm20.launcher2.ui.legacy.widget
-
-import android.content.Context
-import android.util.AttributeSet
-import androidx.compose.foundation.layout.Column
-import androidx.compose.material3.LocalAbsoluteTonalElevation
-import androidx.compose.material3.LocalContentColor
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.unit.dp
-import de.mm20.launcher2.ui.MdcLauncherTheme
-import de.mm20.launcher2.ui.R
-import de.mm20.launcher2.ui.base.ProvideSettings
-import de.mm20.launcher2.ui.launcher.widgets.calendar.CalendarWidget
-
-class CalendarWidget : LauncherWidget {
-
- override val canResize: Boolean
- get() = false
-
- constructor(context: Context) : super(context)
- constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
- constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(
- context,
- attrs,
- defStyleRes
- )
-
- init {
- val composeView = ComposeView(context)
- composeView.setContent {
- MdcLauncherTheme {
- ProvideSettings {
- // TODO: Temporary solution until parent widget card is rewritten in Compose
- CompositionLocalProvider(
- LocalContentColor provides MaterialTheme.colorScheme.onSurface,
- LocalAbsoluteTonalElevation provides 1.dp
- ) {
- Column {
- CalendarWidget()
- }
- }
- }
- }
- }
- addView(composeView)
-
- }
-
-
- override val name: String
- get() = resources.getString(R.string.widget_name_calendar)
-
-
- companion object {
- const val ID = "calendar"
- }
-}
\ No newline at end of file
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/ClockWidget.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/ClockWidget.kt
deleted file mode 100644
index c12e459a..00000000
--- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/ClockWidget.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-package de.mm20.launcher2.ui.legacy.widget
-
-import android.animation.LayoutTransition
-import android.content.Context
-import android.util.AttributeSet
-import android.widget.FrameLayout
-import androidx.compose.ui.platform.ComposeView
-import de.mm20.launcher2.ui.ClockWidget
-import de.mm20.launcher2.ui.MdcLauncherTheme
-
-class ClockWidget : FrameLayout {
-
- constructor(context: Context) : super(context)
- constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
- constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(
- context,
- attrs,
- defStyleRes
- )
-
- val view = ComposeView(context)
-
- init {
- clipToPadding = false
- clipChildren = false
- layoutTransition = LayoutTransition()
-
- val composeView = ComposeView(context)
-
- addView(composeView)
-
- composeView.layoutParams =
- LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
-
-
- composeView.setContent {
- MdcLauncherTheme {
- ClockWidget()
- }
- }
- }
-}
-
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/ExternalWidget.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/ExternalWidget.kt
deleted file mode 100644
index e0bb8b50..00000000
--- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/ExternalWidget.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-package de.mm20.launcher2.ui.legacy.widget
-
-import android.annotation.SuppressLint
-import android.appwidget.AppWidgetHost
-import android.appwidget.AppWidgetHostView
-import android.appwidget.AppWidgetManager
-import android.appwidget.AppWidgetProviderInfo
-import android.content.Context
-import android.os.Bundle
-import android.view.View
-import android.view.ViewGroup
-import android.widget.ListView
-import android.widget.ScrollView
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.view.doOnLayout
-import androidx.core.view.doOnNextLayout
-import androidx.core.view.get
-import androidx.core.view.iterator
-import de.mm20.launcher2.ktx.dp
-import de.mm20.launcher2.widgets.Widget
-
-@SuppressLint("ViewConstructor")
-class ExternalWidget(
- context: Context,
- val widget: Widget,
- host: AppWidgetHost
-) : LauncherWidget(context) {
-
- val widgetInfo: AppWidgetProviderInfo?
-
- val widgetView: View
-
- init {
- val id = widget.data.toInt()
- widgetInfo = AppWidgetManager.getInstance(context.applicationContext).getAppWidgetInfo(id)
- widgetView = host.createView(context.applicationContext, id, widgetInfo)
- ?: View(context)
- if (widgetView is AppWidgetHostView && widgetView.childCount > 0) {
- enableNestedScroll(widgetView[0])
- }
- val h = widget.height * dp
- val params = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, h.toInt())
- val p = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
- layoutParams = params
- widgetView.layoutParams = p
- addView(widgetView)
- }
-
- override fun setLayoutParams(params: ViewGroup.LayoutParams?) {
- super.setLayoutParams(params)
- params ?: return
- doOnNextLayout {
- val width = if (params.width > 0) params.width else it.width
- val height = if (params.height > 0) params.height else widgetInfo?.minHeight ?: it.height
- if (widgetView is AppWidgetHostView) {
- widgetView.updateAppWidgetSize(Bundle(), 0, 0, width, height)
- }
- }
- }
-
- private fun enableNestedScroll(view: View) {
- if (view is ViewGroup) {
- for (child in view.iterator()) {
- enableNestedScroll(child)
- }
- }
- if (view is ListView || view is ScrollView) view.isNestedScrollingEnabled = true
- }
-
- override val canResize: Boolean
- get() = true
- override val name: String
- get() = widgetInfo?.loadLabel(context.packageManager) ?: ""
-
-}
\ No newline at end of file
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/LauncherWidget.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/LauncherWidget.kt
deleted file mode 100644
index 995c74f5..00000000
--- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/LauncherWidget.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package de.mm20.launcher2.ui.legacy.widget
-
-import android.animation.LayoutTransition
-import android.content.Context
-import android.util.AttributeSet
-import android.widget.FrameLayout
-
-abstract class LauncherWidget : FrameLayout {
-
- constructor(context: Context) : super(context)
- constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
- constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
-
- abstract val canResize: Boolean
- abstract val name: String
-
-}
\ No newline at end of file
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/MusicWidget.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/MusicWidget.kt
deleted file mode 100644
index 59c80bff..00000000
--- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/MusicWidget.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package de.mm20.launcher2.ui.legacy.widget
-
-import android.content.Context
-import android.util.AttributeSet
-import android.widget.FrameLayout
-import androidx.compose.foundation.layout.Column
-import androidx.compose.material3.LocalContentColor
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.ui.platform.ComposeView
-import de.mm20.launcher2.ui.MdcLauncherTheme
-import de.mm20.launcher2.ui.R
-import de.mm20.launcher2.ui.launcher.widgets.music.MusicWidget
-
-class MusicWidget : LauncherWidget {
-
- override val canResize: Boolean
- get() = false
- override val name: String
- get() = context.getString(R.string.widget_name_music)
-
- constructor(context: Context) : super(context)
- constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
- constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
-
-
- init {
- val composeView = ComposeView(context)
- composeView.id = FrameLayout.generateViewId()
- composeView.setContent {
- MdcLauncherTheme {
- // TODO: Temporary solution until parent widget card is rewritten in Compose
- CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
- Column {
- MusicWidget()
- }
- }
- }
- }
- addView(composeView)
- }
-
- companion object {
- const val ID = "music"
- }
-}
-
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/WeatherWidget.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/WeatherWidget.kt
deleted file mode 100644
index 8416e2e5..00000000
--- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/WeatherWidget.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-package de.mm20.launcher2.ui.legacy.widget
-
-import android.content.Context
-import android.util.AttributeSet
-import android.widget.FrameLayout
-import androidx.compose.foundation.layout.Column
-import androidx.compose.material3.LocalContentColor
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.ui.platform.ComposeView
-import de.mm20.launcher2.ui.MdcLauncherTheme
-import de.mm20.launcher2.ui.R
-import de.mm20.launcher2.ui.launcher.widgets.weather.WeatherWidget
-
-class WeatherWidget : LauncherWidget {
-
-
- override val canResize: Boolean
- get() = false
- override val name: String
- get() = resources.getString(R.string.widget_name_weather)
-
-
- constructor(context: Context) : super(context)
- constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
- constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
-
-
- init {
- val composeView = ComposeView(context)
- composeView.id = FrameLayout.generateViewId()
- composeView.setContent {
- MdcLauncherTheme {
- // TODO: Temporary solution until parent widget card is rewritten in Compose
- CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
- Column {
- WeatherWidget()
- }
- }
- }
- }
- addView(composeView)
- }
-
- companion object {
- const val ID = "weather"
- }
-}
-
-
diff --git a/ui/src/main/res/layout/view_launcher_scaffold.xml b/ui/src/main/res/layout/view_launcher_scaffold.xml
index c6d21e4c..021aa003 100644
--- a/ui/src/main/res/layout/view_launcher_scaffold.xml
+++ b/ui/src/main/res/layout/view_launcher_scaffold.xml
@@ -37,8 +37,7 @@
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
- android:orientation="vertical"
- android:layout_margin="8dp" />
+ android:orientation="vertical" />
diff --git a/ui/src/main/res/layout/view_widget.xml b/ui/src/main/res/layout/view_widget.xml
deleted file mode 100644
index 17fc4d1f..00000000
--- a/ui/src/main/res/layout/view_widget.xml
+++ /dev/null
@@ -1,80 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/ui/src/main/res/layout/view_widgets.xml b/ui/src/main/res/layout/view_widgets.xml
deleted file mode 100644
index 8a7a783a..00000000
--- a/ui/src/main/res/layout/view_widgets.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/widgets/src/main/java/de/mm20/launcher2/widgets/Module.kt b/widgets/src/main/java/de/mm20/launcher2/widgets/Module.kt
index d1759bc0..75abf6ac 100644
--- a/widgets/src/main/java/de/mm20/launcher2/widgets/Module.kt
+++ b/widgets/src/main/java/de/mm20/launcher2/widgets/Module.kt
@@ -5,5 +5,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val widgetsModule = module {
- single { WidgetRepository(androidContext()) }
+ single { WidgetRepositoryImpl(androidContext(), get()) }
}
\ No newline at end of file
diff --git a/widgets/src/main/java/de/mm20/launcher2/widgets/Widget.kt b/widgets/src/main/java/de/mm20/launcher2/widgets/Widget.kt
index e2dfd28f..cea16880 100644
--- a/widgets/src/main/java/de/mm20/launcher2/widgets/Widget.kt
+++ b/widgets/src/main/java/de/mm20/launcher2/widgets/Widget.kt
@@ -1,32 +1,173 @@
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.ktx.tryStartActivity
-data class Widget(
- val type: WidgetType,
- var data: String,
- var height: Int,
- val label: String = ""
-) {
- constructor(entity: WidgetEntity) : this(
- type = if (entity.type == "internal") WidgetType.INTERNAL else WidgetType. THIRD_PARTY,
- data = entity.data,
- height = entity.height,
- label = entity.label
- )
+sealed class Widget {
+ abstract fun loadLabel(context: Context): String
+ abstract fun toDatabaseEntity(position: Int = -1): WidgetEntity
+ open val isConfigurable: Boolean = false
+ open fun configure(context: Activity, appWidgetHost: AppWidgetHost) {}
- fun toDatabaseEntity(position: Int): WidgetEntity {
+ 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
+ else -> null
+ }
+ } 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
+ )
+ }
+ }
+ }
+}
+
+
+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 = if (type == WidgetType.INTERNAL) "internal" else "3rdparty",
- label = label,
- position = position,
- height = height,
- data = data
+ 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() {
+ 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 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/music"
+ )
+ 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)
+ }
+}
+
+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
)
}
}
-enum class WidgetType {
- INTERNAL,
- THIRD_PARTY
+enum class WidgetType(val value: String) {
+ INTERNAL("internal"),
+ THIRD_PARTY("3rdparty")
}
\ No newline at end of file
diff --git a/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetRepository.kt b/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetRepository.kt
index 3d003361..fb30ab6e 100644
--- a/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetRepository.kt
+++ b/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetRepository.kt
@@ -1,40 +1,79 @@
package de.mm20.launcher2.widgets
import android.content.Context
-import de.mm20.launcher2.widgets.R
import de.mm20.launcher2.database.AppDatabase
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
-import java.util.concurrent.Executors
-class WidgetRepository(
- val context: Context
-) {
+interface WidgetRepository {
+ fun getWidgets(): Flow>
+ fun getInternalWidgets(): List
+ fun saveWidgets(widgets: List)
+ fun addWidget(widget: Widget, position: Int)
+ fun removeWidget(widget: Widget)
+ fun setWidgetHeight(widget: Widget, newHeight: Int)
+}
+
+internal class WidgetRepositoryImpl(
+ private val context: Context,
+ private val database: AppDatabase,
+) : WidgetRepository {
private val scope = CoroutineScope(Job() + Dispatchers.Default)
- fun getWidgets(): Flow> {
- return AppDatabase.getInstance(context).widgetDao()
+ override fun getWidgets(): Flow> {
+ return database.widgetDao()
.getWidgets()
- .map { it.map { Widget(it) } }
+ .map { it.mapNotNull { Widget.fromDatabaseEntity(context, it) } }
}
- fun getInternalWidgets(): List {
- return listOf(
- Widget(WidgetType.INTERNAL, "weather", -1, context.getString(R.string.widget_name_weather)),
- Widget(WidgetType.INTERNAL, "music", -1, context.getString(R.string.widget_name_music)),
- Widget(WidgetType.INTERNAL, "calendar", -1, context.getString(R.string.widget_name_calendar)),
- )
+ override fun getInternalWidgets(): List {
+ return listOf(WeatherWidget, MusicWidget, CalendarWidget)
}
- fun saveWidgets(widgets: List) {
+ override fun saveWidgets(widgets: List) {
scope.launch {
withContext(Dispatchers.IO) {
- AppDatabase.getInstance(context).widgetDao().updateWidgets(widgets.mapIndexed { i, widget -> widget.toDatabaseEntity(i) })
+ database.widgetDao()
+ .updateWidgets(widgets.mapIndexed { i, widget -> widget.toDatabaseEntity(i) })
}
}
}
+
+ override fun addWidget(widget: Widget, position: Int) {
+ scope.launch {
+ withContext(Dispatchers.IO) {
+ database.widgetDao()
+ .insert(widget.toDatabaseEntity(position))
+ }
+ }
+ }
+
+ override fun removeWidget(widget: Widget) {
+ scope.launch {
+ withContext(Dispatchers.IO) {
+ val ent = widget.toDatabaseEntity()
+ database.widgetDao().deleteWidget(
+ ent.type,
+ ent.data
+ )
+ }
+ }
+ }
+
+ override fun setWidgetHeight(widget: Widget, newHeight: Int) {
+ scope.launch {
+ withContext(Dispatchers.IO) {
+ val ent = widget.toDatabaseEntity()
+ database.widgetDao().updateHeight(
+ ent.type,
+ ent.data,
+ newHeight
+ )
+ }
+ }
+ }
+
}
\ No newline at end of file