Refactor widgets

This commit is contained in:
MM20 2022-03-29 18:13:31 +02:00
parent 2e43b871c5
commit 956af82f79
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
21 changed files with 804 additions and 846 deletions

View File

@ -8,36 +8,51 @@
android:height="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <group
android:name="path" android:name="group"
android:pathData="M 3 17.46 L 3 20.5 C 3 20.78 3.22 21 3.5 21 L 6.54 21 C 6.67 21 6.8 20.95 6.89 20.85 L 17.81 9.94 L 14.06 6.19 L 3.15 17.1 C 3.05 17.2 3 17.32 3 17.46 Z" android:pivotX="12"
android:fillColor="#000" android:pivotY="12">
android:strokeWidth="1"/> <path
<path android:name="path"
android:name="path_1" android:pathData="M 3 17.46 L 3 20.5 C 3 20.78 3.22 21 3.5 21 L 6.54 21 C 6.67 21 6.8 20.95 6.89 20.85 L 17.81 9.94 L 14.06 6.19 L 3.15 17.1 C 3.05 17.2 3 17.32 3 17.46 Z M 20.71 7.04 C 21.1 6.65 21.1 6.02 20.71 5.63 L 18.37 3.29 C 17.98 2.9 17.35 2.9 16.96 3.29 L 15.13 5.12 L 18.88 8.87 L 20.71 7.04 Z"
android:pathData="M 20.71 7.04 C 21.1 6.65 21.1 6.02 20.71 5.63 L 18.37 3.29 C 17.98 2.9 17.35 2.9 16.96 3.29 L 15.13 5.12 L 18.88 8.87 L 20.71 7.04 Z" android:fillColor="#000"
android:fillColor="#000000" android:strokeWidth="1"/>
android:strokeWidth="1"/> <path
android:name="path_1"
android:pathData=""
android:fillColor="#000000"/>
</group>
</vector> </vector>
</aapt:attr> </aapt:attr>
<target android:name="path"> <target android:name="path">
<aapt:attr name="android:animation"> <aapt:attr name="android:animation">
<objectAnimator <objectAnimator
android:propertyName="pathData" android:propertyName="pathData"
android:duration="200" android:duration="100"
android:valueFrom="M 15.894 8.024 C 15.283 7.413 14.671 6.801 14.06 6.19 L 3.15 17.1 C 3.05 17.2 3 17.32 3 17.46 L 3 17.46 C 3 18.473 3 19.487 3 20.5 C 3 20.78 3.22 21 3.5 21 C 4.513 21 5.527 21 6.54 21 C 6.67 21 6.8 20.95 6.89 20.85 C 10.53 17.213 14.17 13.577 17.81 9.94 C 17.171 9.301 16.533 8.663 15.894 8.024" android:valueFrom="M 11 18 C 11 14.007 11 9.578 11 6 C 11 5.45 11.45 5 12 5 C 12.55 5 13 5.45 13 6 L 13 18 C 13 18.298 12.868 18.567 12.659 18.751 C 12.482 18.906 12.252 19 12 19 C 12 19 12 19 12 19 C 11.706 19 11.441 18.872 11.257 18.668 C 11.098 18.491 11 18.256 11 18 C 11 18 11 18 11 18 M 12.001 3.13 L 12.001 3.13 L 12.001 3.13 L 12.001 3.13 C 12.001 3.13 12.001 3.13 12.001 3.13 L 12.001 3.13 C 12.001 3.13 12.001 3.13 12.001 3.13"
android:valueTo="M 19 12 C 19 11.45 18.55 11 18 11 L 6.014 11 C 6.014 11 6.014 11 6.014 11 L 6 11 C 5.456 11 5.01 11.44 5 11.982 C 5 11.988 5 11.994 5 12 C 5 12.55 5.45 13 6 13 C 6 13 6 13 6 13 C 10 13 14 13 18 13 C 18.55 13 19 12.55 19 12" android:valueTo="M 9.35 21.865 C 9.35 16.722 9.35 11.579 9.35 6.436 C 10.233 6.436 11.117 6.436 12.001 6.436 C 12.885 6.436 13.769 6.436 14.653 6.436 L 14.646 21.872 C 14.653 22.006 14.596 22.133 14.504 22.225 C 13.788 22.942 13.071 23.658 12.355 24.375 C 12.157 24.573 11.846 24.573 11.648 24.375 C 10.931 23.658 10.215 22.942 9.498 22.225 C 9.498 22.225 9.498 22.225 9.498 22.225 C 9.399 22.126 9.35 22.006 9.35 21.865 M 14.653 2.334 L 14.653 4.922 L 9.35 4.922 L 9.35 2.334 C 9.35 1.783 9.795 1.337 10.347 1.337 L 13.656 1.337 C 14.207 1.337 14.653 1.783 14.653 2.334"
android:valueType="pathType" android:valueType="pathType"
android:interpolator="@android:interpolator/fast_out_slow_in"/> android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr> </aapt:attr>
</target> </target>
<target android:name="group">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="rotation"
android:duration="200"
android:valueFrom="0"
android:valueTo="45"
android:valueType="floatType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
<target android:name="path_1"> <target android:name="path_1">
<aapt:attr name="android:animation"> <aapt:attr name="android:animation">
<objectAnimator <objectAnimator
android:propertyName="pathData" android:propertyName="pathData"
android:duration="200" android:duration="200"
android:valueFrom="M 18.88 8.87 C 19.49 8.26 20.1 7.65 20.71 7.04 C 20.71 7.04 20.71 7.04 20.71 7.04 C 21.1 6.65 21.1 6.02 20.71 5.63 C 19.93 4.85 19.15 4.07 18.37 3.29 C 17.98 2.9 17.35 2.9 16.96 3.29 C 16.35 3.9 15.74 4.51 15.13 5.12 L 18.88 8.87" android:valueFrom="M 18 13 C 14.007 13 9.578 13 6 13 C 5.45 13 5 12.55 5 12 C 5 11.45 5.45 11 6 11 L 18 11 C 18.55 11 19 11.45 19 12 C 19 12.55 18.55 13 18 13 Z"
android:valueTo="M 11 18 C 11 18.55 11.45 19 12 19 C 12.029 19 12.058 18.999 12.086 18.996 C 12.596 18.952 13 18.521 13 18 C 13 14 13 10 13 6 C 13 5.45 12.55 5 12 5 C 11.45 5 11 5.45 11 6 L 11 18" android:valueTo="M 12 13 C 12 13 12 13 12 13 C 11.45 13 11 12.55 11 12 C 11 11.45 11.45 11 12 11 L 12 11 C 12.55 11 13 11.45 13 12 C 13 12.55 12.55 13 12 13 Z"
android:valueType="pathType" android:valueType="pathType"
android:interpolator="@android:interpolator/fast_out_slow_in"/> android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr> </aapt:attr>

View File

@ -1,7 +1,9 @@
package de.mm20.launcher2.database package de.mm20.launcher2.database
import androidx.lifecycle.LiveData import androidx.room.Dao
import androidx.room.* import androidx.room.Insert
import androidx.room.Query
import androidx.room.Transaction
import de.mm20.launcher2.database.entities.WidgetEntity import de.mm20.launcher2.database.entities.WidgetEntity
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -19,6 +21,16 @@ interface WidgetDao {
@Insert @Insert
fun insertAll(widgets: List<WidgetEntity>) fun insertAll(widgets: List<WidgetEntity>)
@Insert
fun insert(widget: WidgetEntity)
@Query("DELETE FROM Widget") @Query("DELETE FROM Widget")
fun deleteAll() 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)
} }

View File

@ -173,7 +173,7 @@ class LauncherScaffoldView @JvmOverloads constructor(
} }
widgetsViewModel.isEditMode.observe(context) { widgetsViewModel.isEditMode.observe(context) {
OneShotLayoutTransition.run(binding.scrollContainer) //OneShotLayoutTransition.run(binding.scrollContainer)
if (it) { if (it) {
binding.scrollView.setOnTouchListener(null) binding.scrollView.setOnTouchListener(null)
binding.searchBar.visibility = View.INVISIBLE binding.searchBar.visibility = View.INVISIBLE

View File

@ -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)
}
)
)
}
}
}
}
}
}
}
}

View File

@ -1,11 +1,17 @@
package de.mm20.launcher2.ui.launcher.widgets package de.mm20.launcher2.ui.launcher.widgets
import android.appwidget.AppWidgetManager
import android.content.Context
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData import androidx.lifecycle.liveData
import de.mm20.launcher2.widgets.ExternalWidget
import de.mm20.launcher2.widgets.Widget import de.mm20.launcher2.widgets.Widget
import de.mm20.launcher2.widgets.WidgetRepository 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.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
@ -20,11 +26,47 @@ class WidgetsVM : ViewModel(), KoinComponent {
isEditMode.value = editMode isEditMode.value = editMode
} }
fun saveWidgets(widgets: List<Widget>) { 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<Widget> {
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) widgetRepository.saveWidgets(widgets)
} }
fun getInternalWidgets(): List<Widget> { fun moveDown(index: Int) {
return widgetRepository.getInternalWidgets() val widgets = widgets.value?.toMutableList() ?: return
val widget = widgets.removeAt(index)
widgets.add(index + 1, widget)
widgetRepository.saveWidgets(widgets)
} }
} }

View File

@ -1,7 +1,5 @@
package de.mm20.launcher2.ui.launcher.widgets package de.mm20.launcher2.ui.launcher.widgets
import android.animation.LayoutTransition
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
@ -9,259 +7,317 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.widget.FrameLayout
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.iterator import androidx.compose.animation.AnimatedVisibility
import androidx.lifecycle.Lifecycle import androidx.compose.animation.animateContentSize
import androidx.lifecycle.lifecycleScope import androidx.compose.animation.core.Animatable
import androidx.lifecycle.repeatOnLifecycle import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
import com.afollestad.materialdialogs.MaterialDialog import androidx.compose.animation.graphics.res.animatedVectorResource
import com.afollestad.materialdialogs.list.listItems import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
import com.balsikandar.crashreporter.CrashReporter import androidx.compose.animation.graphics.vector.AnimatedImageVector
import de.mm20.launcher2.ktx.dp import androidx.compose.foundation.clickable
import de.mm20.launcher2.transition.ChangingLayoutTransition import androidx.compose.foundation.gestures.rememberDraggableState
import de.mm20.launcher2.transition.OneShotLayoutTransition 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.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.launcher.widgets.picker.PickAppWidgetActivity
import de.mm20.launcher2.ui.legacy.component.WidgetView import de.mm20.launcher2.widgets.ExternalWidget
import de.mm20.launcher2.widgets.Widget
import de.mm20.launcher2.widgets.WidgetType
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.*
import kotlin.math.roundToInt
@OptIn(ExperimentalAnimationGraphicsApi::class)
class WidgetsView @JvmOverloads constructor( class WidgetsView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null context: Context, attrs: AttributeSet? = null
) : LinearLayout(context, attrs) { ) : FrameLayout(context, attrs) {
private val binding = ViewWidgetsBinding.inflate(LayoutInflater.from(context), this)
private val widgetHost: AppWidgetHost = AppWidgetHost(context.applicationContext, 44203) private val widgetHost: AppWidgetHost = AppWidgetHost(context.applicationContext, 44203)
private val viewModel: WidgetsVM by (context as AppCompatActivity).viewModels() private val viewModel: WidgetsVM by (context as AppCompatActivity).viewModels()
private lateinit var widgets: MutableList<Widget>
private val pickWidgetLauncher: ActivityResultLauncher<Intent> private val pickWidgetLauncher: ActivityResultLauncher<Intent>
private val clockWidgetHeight = MutableLiveData(0)
init { init {
context as AppCompatActivity context as AppCompatActivity
pickWidgetLauncher = context.registerForActivityResult( pickWidgetLauncher = context.registerForActivityResult(
ActivityResultContracts.StartActivityForResult() ActivityResultContracts.StartActivityForResult()
) { ) {
val data = it.data ?: return@registerForActivityResult 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 (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return@registerForActivityResult
if (it.resultCode == Activity.RESULT_OK) { 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) { val composeView = ComposeView(context)
if (it) { composeView.setContent {
OneShotLayoutTransition.run(binding.widgetList) MdcLauncherTheme {
binding.clockWidget.visibility = View.GONE 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()) { AnimatedVisibility(!editMode) {
if (v is WidgetView) { Box(
v.editMode = true modifier = Modifier
v.onResizeModeChange = { .fillMaxWidth()
OneShotLayoutTransition.run(binding.widgetList) .height(clockHeight.toDp()),
OneShotLayoutTransition.run(this) 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<Float?> { 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)
}
} }
} }
} }
addView(composeView)
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)
}
} }
fun setClockWidgetHeight(height: Int) { fun setClockWidgetHeight(height: Int) {
val params = binding.clockWidget.layoutParams clockWidgetHeight.value = height
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)
}
} }
} }

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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"
}
}

View File

@ -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()
}
}
}
}

View File

@ -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) ?: ""
}

View File

@ -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
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -37,8 +37,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clipChildren="false" android:clipChildren="false"
android:clipToPadding="false" android:clipToPadding="false"
android:orientation="vertical" android:orientation="vertical" />
android:layout_margin="8dp" />
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>

View File

@ -1,80 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/widgetWrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:id="@+id/widgetControlPanel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:visibility="gone">
<ImageView
android:id="@+id/widgetDragHandle"
style="?iconStyle"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:focusable="true"
android:foreground="?selectableItemBackgroundBorderless"
android:padding="12dp"
android:src="@drawable/ic_drag_handle"/>
<ImageView
android:id="@+id/widgetActionResize"
style="?iconStyle"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="end"
android:layout_marginStart="4dp"
android:layout_marginEnd="56dp"
android:clickable="true"
android:focusable="true"
android:foreground="?selectableItemBackgroundBorderless"
android:padding="12dp"
android:src="@drawable/ic_widget_resize"/>
<ImageView
android:id="@+id/widgetActionRemove"
style="?iconStyle"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="end"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:clickable="true"
android:focusable="true"
android:foreground="?selectableItemBackgroundBorderless"
android:padding="12dp"
android:src="@drawable/ic_delete"/>
</FrameLayout>
<TextView
android:id="@+id/widgetName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?textAppearanceTitleLarge"
android:visibility="gone"/>
<de.mm20.launcher2.ui.legacy.view.WidgetResizeDragView
android:id="@+id/widgetResizeDragHandle"
style="?iconStyle"
android:layout_width="match_parent"
android:layout_height="24dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:src="@drawable/ic_resize_drag_handle"
android:visibility="gone"/>
</LinearLayout>
</merge>

View File

@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:layout_height="wrap_content"
tools:layout_width="match_parent">
<de.mm20.launcher2.ui.legacy.widget.ClockWidget
android:id="@+id/clockWidget"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.jmedeisis.draglinearlayout.DragLinearLayout
android:id="@+id/widgetList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="vertical">
<!-- Widgets will be added here -->
</com.jmedeisis.draglinearlayout.DragLinearLayout>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/fabEditWidget"
app:icon="@drawable/ic_edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:text="@string/menu_edit_widgets" />
</merge>

View File

@ -5,5 +5,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module import org.koin.dsl.module
val widgetsModule = module { val widgetsModule = module {
single { WidgetRepository(androidContext()) } single<WidgetRepository> { WidgetRepositoryImpl(androidContext(), get()) }
} }

View File

@ -1,32 +1,173 @@
package de.mm20.launcher2.widgets 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.WidgetEntity
import de.mm20.launcher2.ktx.tryStartActivity
data class Widget( sealed class Widget {
val type: WidgetType, abstract fun loadLabel(context: Context): String
var data: String, abstract fun toDatabaseEntity(position: Int = -1): WidgetEntity
var height: Int, open val isConfigurable: Boolean = false
val label: String = "" open fun configure(context: Activity, appWidgetHost: AppWidgetHost) {}
) {
constructor(entity: WidgetEntity) : this(
type = if (entity.type == "internal") WidgetType.INTERNAL else WidgetType. THIRD_PARTY,
data = entity.data,
height = entity.height,
label = entity.label
)
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( return WidgetEntity(
type = if (type == WidgetType.INTERNAL) "internal" else "3rdparty", type = WidgetType.INTERNAL.value,
label = label, data = "weather",
position = position, height = -1,
height = height, position = position
data = data )
}
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 { enum class WidgetType(val value: String) {
INTERNAL, INTERNAL("internal"),
THIRD_PARTY THIRD_PARTY("3rdparty")
} }

View File

@ -1,40 +1,79 @@
package de.mm20.launcher2.widgets package de.mm20.launcher2.widgets
import android.content.Context import android.content.Context
import de.mm20.launcher2.widgets.R
import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.AppDatabase
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import java.util.concurrent.Executors
class WidgetRepository( interface WidgetRepository {
val context: Context fun getWidgets(): Flow<List<Widget>>
) { fun getInternalWidgets(): List<Widget>
fun saveWidgets(widgets: List<Widget>)
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) private val scope = CoroutineScope(Job() + Dispatchers.Default)
fun getWidgets(): Flow<List<Widget>> { override fun getWidgets(): Flow<List<Widget>> {
return AppDatabase.getInstance(context).widgetDao() return database.widgetDao()
.getWidgets() .getWidgets()
.map { it.map { Widget(it) } } .map { it.mapNotNull { Widget.fromDatabaseEntity(context, it) } }
} }
fun getInternalWidgets(): List<Widget> { override fun getInternalWidgets(): List<Widget> {
return listOf( return listOf(WeatherWidget, MusicWidget, CalendarWidget)
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)),
)
} }
fun saveWidgets(widgets: List<Widget>) { override fun saveWidgets(widgets: List<Widget>) {
scope.launch { scope.launch {
withContext(Dispatchers.IO) { 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
)
}
}
}
} }