Note widget: add dismiss action

This commit is contained in:
MM20 2023-04-27 18:45:52 +02:00
parent 9f675da337
commit 5211f5bced
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
7 changed files with 119 additions and 23 deletions

View File

@ -91,8 +91,8 @@ fun WidgetColumn(
widget = widget, widget = widget,
appWidgetHost = widgetHost, appWidgetHost = widgetHost,
editMode = editMode, editMode = editMode,
onWidgetAdd = { onWidgetAdd = { widget, offset ->
viewModel.addWidget(it, i + 1) viewModel.addWidget(widget, i + offset)
}, },
onWidgetRemove = { onWidgetRemove = {
if (widget is AppWidget) { if (widget is AppWidget) {

View File

@ -62,7 +62,7 @@ fun WidgetItem(
appWidgetHost: AppWidgetHost, appWidgetHost: AppWidgetHost,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
editMode: Boolean = false, editMode: Boolean = false,
onWidgetAdd: (widget: Widget) -> Unit = {}, onWidgetAdd: (widget: Widget, offset: Int) -> Unit = {_, _ ->},
onWidgetUpdate: (widget: Widget) -> Unit = {}, onWidgetUpdate: (widget: Widget) -> Unit = {},
onWidgetRemove: () -> Unit = {}, onWidgetRemove: () -> Unit = {},
draggableState: DraggableState = rememberDraggableState {}, draggableState: DraggableState = rememberDraggableState {},
@ -163,13 +163,7 @@ fun WidgetItem(
is NotesWidget -> { is NotesWidget -> {
NotesWidget( NotesWidget(
widget, widget,
onNewNote = { onWidgetAdd = onWidgetAdd,
val newWidget = NotesWidget(
id = UUID.randomUUID(),
widget.config.copy(storedText = "")
)
onWidgetAdd(newWidget)
}
) )
} }

View File

@ -3,9 +3,12 @@ package de.mm20.launcher2.ui.launcher.widgets.notes
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add 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.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -26,24 +32,35 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.markdown.MarkdownEditor 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.NotesWidget
import de.mm20.launcher2.widgets.Widget
import kotlinx.coroutines.launch
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.UUID
@Composable @Composable
fun NotesWidget( fun NotesWidget(
widget: NotesWidget, widget: NotesWidget,
onNewNote: () -> Unit, onWidgetAdd: (widget: Widget, offset: Int) -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val snackbarHostState = LocalSnackbarHostState.current
val lifecycleOwner = LocalLifecycleOwner.current
val viewModel: NotesWidgetVM = val viewModel: NotesWidgetVM =
viewModel(key = "notes-widget-${widget.id}", factory = NotesWidgetVM.Factory) viewModel(key = "notes-widget-${widget.id}", factory = NotesWidgetVM.Factory)
val isLastWidget by viewModel.isLastNoteWidget.collectAsState(null)
LaunchedEffect(widget) { LaunchedEffect(widget) {
viewModel.updateWidget(widget) viewModel.updateWidget(widget)
} }
@ -57,20 +74,45 @@ fun NotesWidget(
val text by viewModel.noteText val text by viewModel.noteText
Column { Column {
MarkdownEditor( Row(
value = text, verticalAlignment = Alignment.CenterVertically,
onValueChange = { viewModel.setText(it) },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .heightIn(min = 64.dp)
.padding(16.dp), .animateContentSize(),
placeholder = { ) {
Text(stringResource(R.string.notes_widget_placeholder)) 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()) { AnimatedVisibility(text.isNotBlank()) {
var showMenu by remember { mutableStateOf(false) } 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 { Box {
IconButton(onClick = { showMenu = true }) { IconButton(onClick = { showMenu = true }) {
Icon(Icons.Rounded.MoreVert, null) Icon(Icons.Rounded.MoreVert, null)
@ -82,7 +124,11 @@ fun NotesWidget(
Icon(Icons.Rounded.Add, null) Icon(Icons.Rounded.Add, null)
}, },
onClick = { onClick = {
onNewNote() val newWidget = NotesWidget(
id = UUID.randomUUID(),
widget.config.copy(storedText = "")
)
onWidgetAdd(newWidget, 1)
showMenu = false showMenu = false
}, },
) )
@ -103,11 +149,39 @@ fun NotesWidget(
}, },
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text("Dismiss") }, text = { Text(stringResource(R.string.notes_widget_action_dismiss)) },
leadingIcon = { leadingIcon = {
Icon(Icons.Rounded.Delete, null) 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
},
) )
} }
} }

View File

@ -14,6 +14,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow 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 kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
@ -25,6 +28,10 @@ class NotesWidgetVM(
val noteText = mutableStateOf(widget.value?.config?.storedText ?: "") 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) { fun updateWidget(widget: NotesWidget) {
val oldId = this.widget.value?.id val oldId = this.widget.value?.id
this.widget.value = widget this.widget.value = widget
@ -53,6 +60,11 @@ class NotesWidgetVM(
} }
} }
fun dismissNote() {
widgetsService.removeWidget(widget.value ?: return)
}
companion object: KoinComponent { companion object: KoinComponent {
val Factory = viewModelFactory { val Factory = viewModelFactory {
initializer { initializer {

View File

@ -51,4 +51,7 @@ interface WidgetDao {
@Query("SELECT EXISTS(SELECT 1 FROM Widget WHERE type = :type)") @Query("SELECT EXISTS(SELECT 1 FROM Widget WHERE type = :type)")
fun exists(type: String): Flow<Boolean> fun exists(type: String): Flow<Boolean>
@Query("SELECT COUNT(*) FROM Widget WHERE type = :type")
fun count(type: String): Flow<Int>
} }

View File

@ -21,6 +21,7 @@ interface WidgetRepository {
fun set(widgets: List<Widget>, parentId: UUID? = null) fun set(widgets: List<Widget>, parentId: UUID? = null)
fun exists(type: String): Flow<Boolean> fun exists(type: String): Flow<Boolean>
fun count(type: String): Flow<Int>
suspend fun export(toDir: File) suspend fun export(toDir: File)
suspend fun import(fromDir: File) suspend fun import(fromDir: File)
@ -85,6 +86,12 @@ internal class WidgetRepositoryImpl(
return dao.exists(type = type) return dao.exists(type = type)
} }
override fun count(type: String): Flow<Int> {
val dao = database.widgetDao()
return dao.count(type = type)
}
override suspend fun export(toDir: File) = withContext(Dispatchers.IO) { override suspend fun export(toDir: File) = withContext(Dispatchers.IO) {
val dao = database.backupDao() val dao = database.backupDao()

View File

@ -80,6 +80,12 @@ class WidgetsService(
} }
} }
fun countWidgets(type: String) = widgetRepository.count(type)
fun removeWidget(widget: Widget) {
widgetRepository.delete(widget)
}
companion object { companion object {
const val AppWidgetHostId = 44203 const val AppWidgetHostId = 44203
} }