From 5211f5bced74ad303debddd09083ecb6ab8a9e23 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Thu, 27 Apr 2023 18:45:52 +0200 Subject: [PATCH] Note widget: add dismiss action --- .../ui/launcher/widgets/WidgetColumn.kt | 4 +- .../ui/launcher/widgets/WidgetItem.kt | 10 +- .../ui/launcher/widgets/notes/NotesWidget.kt | 100 +++++++++++++++--- .../launcher/widgets/notes/NotesWidgetVM.kt | 12 +++ .../de/mm20/launcher2/database/WidgetDao.kt | 3 + .../launcher2/widgets/WidgetRepository.kt | 7 ++ .../services/widgets/WidgetsService.kt | 6 ++ 7 files changed, 119 insertions(+), 23 deletions(-) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt index 2d9f0687..f577703a 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt @@ -91,8 +91,8 @@ fun WidgetColumn( widget = widget, appWidgetHost = widgetHost, editMode = editMode, - onWidgetAdd = { - viewModel.addWidget(it, i + 1) + onWidgetAdd = { widget, offset -> + viewModel.addWidget(widget, i + offset) }, onWidgetRemove = { if (widget is AppWidget) { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetItem.kt index ece74ab1..2707911d 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetItem.kt @@ -62,7 +62,7 @@ fun WidgetItem( appWidgetHost: AppWidgetHost, modifier: Modifier = Modifier, editMode: Boolean = false, - onWidgetAdd: (widget: Widget) -> Unit = {}, + onWidgetAdd: (widget: Widget, offset: Int) -> Unit = {_, _ ->}, onWidgetUpdate: (widget: Widget) -> Unit = {}, onWidgetRemove: () -> Unit = {}, draggableState: DraggableState = rememberDraggableState {}, @@ -163,13 +163,7 @@ fun WidgetItem( is NotesWidget -> { NotesWidget( widget, - onNewNote = { - val newWidget = NotesWidget( - id = UUID.randomUUID(), - widget.config.copy(storedText = "") - ) - onWidgetAdd(newWidget) - } + onWidgetAdd = onWidgetAdd, ) } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidget.kt index e3f23d82..74db58f0 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidget.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidget.kt @@ -3,9 +3,12 @@ package de.mm20.launcher2.ui.launcher.widgets.notes import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Add @@ -16,9 +19,12 @@ import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -26,24 +32,35 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewmodel.compose.viewModel import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.component.markdown.MarkdownEditor +import de.mm20.launcher2.ui.locals.LocalSnackbarHostState import de.mm20.launcher2.widgets.NotesWidget +import de.mm20.launcher2.widgets.Widget +import kotlinx.coroutines.launch import java.time.ZonedDateTime import java.time.format.DateTimeFormatter +import java.util.UUID @Composable fun NotesWidget( widget: NotesWidget, - onNewNote: () -> Unit, + onWidgetAdd: (widget: Widget, offset: Int) -> Unit, ) { val context = LocalContext.current + val snackbarHostState = LocalSnackbarHostState.current + val lifecycleOwner = LocalLifecycleOwner.current + val viewModel: NotesWidgetVM = viewModel(key = "notes-widget-${widget.id}", factory = NotesWidgetVM.Factory) + val isLastWidget by viewModel.isLastNoteWidget.collectAsState(null) + LaunchedEffect(widget) { viewModel.updateWidget(widget) } @@ -57,20 +74,45 @@ fun NotesWidget( val text by viewModel.noteText Column { - MarkdownEditor( - value = text, - onValueChange = { viewModel.setText(it) }, + Row( + verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - placeholder = { - Text(stringResource(R.string.notes_widget_placeholder)) + .heightIn(min = 64.dp) + .animateContentSize(), + ) { + MarkdownEditor( + value = text, + onValueChange = { viewModel.setText(it) }, + modifier = Modifier + .weight(1f) + .padding(16.dp), + placeholder = { + Text( + stringResource(R.string.notes_widget_placeholder), + ) + + } + ) + AnimatedVisibility(isLastWidget == false && text.isBlank()) { + IconButton( + onClick = { + viewModel.dismissNote() + }, + modifier = Modifier.padding(8.dp) + ) { + Icon(Icons.Rounded.Delete, null) + } } - ) + } AnimatedVisibility(text.isNotBlank()) { var showMenu by remember { mutableStateOf(false) } - Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(start = 8.dp, end = 8.dp, bottom = 8.dp), + contentAlignment = Alignment.CenterEnd + ) { Box { IconButton(onClick = { showMenu = true }) { Icon(Icons.Rounded.MoreVert, null) @@ -82,7 +124,11 @@ fun NotesWidget( Icon(Icons.Rounded.Add, null) }, onClick = { - onNewNote() + val newWidget = NotesWidget( + id = UUID.randomUUID(), + widget.config.copy(storedText = "") + ) + onWidgetAdd(newWidget, 1) showMenu = false }, ) @@ -103,11 +149,39 @@ fun NotesWidget( }, ) DropdownMenuItem( - text = { Text("Dismiss") }, + text = { Text(stringResource(R.string.notes_widget_action_dismiss)) }, leadingIcon = { Icon(Icons.Rounded.Delete, null) }, - onClick = { /*TODO*/ }, + onClick = { + if (isLastWidget == false) { + viewModel.dismissNote() + lifecycleOwner.lifecycleScope.launch { + val result = snackbarHostState.showSnackbar( + message = context.getString(R.string.notes_widget_dismissed), + actionLabel = context.getString(R.string.action_undo), + duration = SnackbarDuration.Short, + ) + if (result == SnackbarResult.ActionPerformed) { + onWidgetAdd(widget, 0) + } + } + } else { + val content = text + viewModel.setText("") + lifecycleOwner.lifecycleScope.launch { + val result = snackbarHostState.showSnackbar( + message = context.getString(R.string.notes_widget_dismissed), + actionLabel = context.getString(R.string.action_undo), + duration = SnackbarDuration.Short, + ) + if (result == SnackbarResult.ActionPerformed) { + viewModel.setText(content) + } + } + } + showMenu = false + }, ) } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidgetVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidgetVM.kt index e982c8a0..4872951a 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidgetVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidgetVM.kt @@ -14,6 +14,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.get @@ -25,6 +28,10 @@ class NotesWidgetVM( val noteText = mutableStateOf(widget.value?.config?.storedText ?: "") + val isLastNoteWidget = widgetsService.countWidgets(NotesWidget.Type).map { + it == 1 + }.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1) + fun updateWidget(widget: NotesWidget) { val oldId = this.widget.value?.id this.widget.value = widget @@ -53,6 +60,11 @@ class NotesWidgetVM( } } + fun dismissNote() { + widgetsService.removeWidget(widget.value ?: return) + } + + companion object: KoinComponent { val Factory = viewModelFactory { initializer { diff --git a/core/database/src/main/java/de/mm20/launcher2/database/WidgetDao.kt b/core/database/src/main/java/de/mm20/launcher2/database/WidgetDao.kt index 429b98b9..0501f465 100644 --- a/core/database/src/main/java/de/mm20/launcher2/database/WidgetDao.kt +++ b/core/database/src/main/java/de/mm20/launcher2/database/WidgetDao.kt @@ -51,4 +51,7 @@ interface WidgetDao { @Query("SELECT EXISTS(SELECT 1 FROM Widget WHERE type = :type)") fun exists(type: String): Flow + @Query("SELECT COUNT(*) FROM Widget WHERE type = :type") + fun count(type: String): Flow + } \ No newline at end of file diff --git a/data/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetRepository.kt b/data/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetRepository.kt index 67955409..28a3963a 100644 --- a/data/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetRepository.kt +++ b/data/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetRepository.kt @@ -21,6 +21,7 @@ interface WidgetRepository { fun set(widgets: List, parentId: UUID? = null) fun exists(type: String): Flow + fun count(type: String): Flow suspend fun export(toDir: File) suspend fun import(fromDir: File) @@ -85,6 +86,12 @@ internal class WidgetRepositoryImpl( return dao.exists(type = type) } + override fun count(type: String): Flow { + val dao = database.widgetDao() + return dao.count(type = type) + + } + override suspend fun export(toDir: File) = withContext(Dispatchers.IO) { val dao = database.backupDao() diff --git a/services/widgets/src/main/java/de/mm20/launcher2/services/widgets/WidgetsService.kt b/services/widgets/src/main/java/de/mm20/launcher2/services/widgets/WidgetsService.kt index 5c8e008e..b4184257 100644 --- a/services/widgets/src/main/java/de/mm20/launcher2/services/widgets/WidgetsService.kt +++ b/services/widgets/src/main/java/de/mm20/launcher2/services/widgets/WidgetsService.kt @@ -80,6 +80,12 @@ class WidgetsService( } } + fun countWidgets(type: String) = widgetRepository.count(type) + + fun removeWidget(widget: Widget) { + widgetRepository.delete(widget) + } + companion object { const val AppWidgetHostId = 44203 }