feat(mail): «Создать задачу» и «Создать событие» из письма
Паритет живого веба: пункты в меню действий письма (иконки темы create-task-black/calendar-black) → модалки в стиле темы (заголовок 18/600, плашки-выбор списка/календаря #F5F5F5 r10, дата через DatePicker, «Весь день», время ЧЧ:ММ). Префилл: название = тема письма, описание = «Из письма от <отправитель>». Сохранение через существующие TasksRepository.createTask / CalendarRepository.saveEvent (CalDAV) — feature:mail теперь зависит от feature:tasks и feature:calendar (цикла нет). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,9 @@ dependencies {
|
|||||||
implementation project(':core:designsystem')
|
implementation project(':core:designsystem')
|
||||||
implementation project(':feature:files')
|
implementation project(':feature:files')
|
||||||
implementation project(':feature:contacts')
|
implementation project(':feature:contacts')
|
||||||
|
// создание задач/событий из письма (живой веб-паритет)
|
||||||
|
implementation project(':feature:tasks')
|
||||||
|
implementation project(':feature:calendar')
|
||||||
implementation libs.coroutines.android
|
implementation libs.coroutines.android
|
||||||
implementation libs.json
|
implementation libs.json
|
||||||
def composeBom = platform(libs.compose.bom)
|
def composeBom = platform(libs.compose.bom)
|
||||||
|
|||||||
@@ -982,6 +982,8 @@ fun MailMessageSenderCard(
|
|||||||
onMove: () -> Unit,
|
onMove: () -> Unit,
|
||||||
onSnooze: () -> Unit,
|
onSnooze: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
onCreateTask: (() -> Unit)? = null,
|
||||||
|
onCreateEvent: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val base = serverUrl.trimEnd('/')
|
val base = serverUrl.trimEnd('/')
|
||||||
var headersExpanded by remember { mutableStateOf(false) }
|
var headersExpanded by remember { mutableStateOf(false) }
|
||||||
@@ -1126,6 +1128,8 @@ fun MailMessageSenderCard(
|
|||||||
onEditTags = onEditTags,
|
onEditTags = onEditTags,
|
||||||
onMove = onMove,
|
onMove = onMove,
|
||||||
onSnooze = onSnooze,
|
onSnooze = onSnooze,
|
||||||
|
onCreateTask = onCreateTask,
|
||||||
|
onCreateEvent = onCreateEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1185,6 +1189,8 @@ fun MailMessageActionsMenu(
|
|||||||
onEditTags: () -> Unit,
|
onEditTags: () -> Unit,
|
||||||
onMove: () -> Unit,
|
onMove: () -> Unit,
|
||||||
onSnooze: () -> Unit,
|
onSnooze: () -> Unit,
|
||||||
|
onCreateTask: (() -> Unit)? = null,
|
||||||
|
onCreateEvent: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
@@ -1238,6 +1244,18 @@ fun MailMessageActionsMenu(
|
|||||||
onDismiss()
|
onDismiss()
|
||||||
onEditTags()
|
onEditTags()
|
||||||
}
|
}
|
||||||
|
onCreateTask?.let { create ->
|
||||||
|
MailMessageIconMenuItem(serverUrl, "create-task-black.svg", "Создать задачу") {
|
||||||
|
onDismiss()
|
||||||
|
create()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onCreateEvent?.let { create ->
|
||||||
|
MailMessageIconMenuItem(serverUrl, "calendar-black.svg", "Создать событие") {
|
||||||
|
onDismiss()
|
||||||
|
create()
|
||||||
|
}
|
||||||
|
}
|
||||||
MailMessageIconMenuItem(serverUrl, "folder-black.svg", "Переместить сообщение") {
|
MailMessageIconMenuItem(serverUrl, "folder-black.svg", "Переместить сообщение") {
|
||||||
onDismiss()
|
onDismiss()
|
||||||
onMove()
|
onMove()
|
||||||
|
|||||||
@@ -0,0 +1,325 @@
|
|||||||
|
package ru.forbion.f7cloud.feature.mail
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
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.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.CheckboxDefaults
|
||||||
|
import androidx.compose.material3.DatePicker
|
||||||
|
import androidx.compose.material3.DatePickerDialog
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.rememberDatePickerState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import ru.forbion.f7cloud.core.designsystem.F7Colors
|
||||||
|
import ru.forbion.f7cloud.core.designsystem.F7OutlinedField
|
||||||
|
import ru.forbion.f7cloud.core.designsystem.F7PrimaryButton
|
||||||
|
import ru.forbion.f7cloud.core.designsystem.F7SecondaryButton
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
/** Выбор из вариантов «плашками» — как строки в модалках живой темы (#F5F5F5, r10, 36dp). */
|
||||||
|
@Composable
|
||||||
|
private fun <T> PickerRows(
|
||||||
|
options: List<T>,
|
||||||
|
selected: T?,
|
||||||
|
label: (T) -> String,
|
||||||
|
onSelect: (T) -> Unit,
|
||||||
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
options.forEach { option ->
|
||||||
|
val isSelected = option == selected
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 36.dp)
|
||||||
|
.clip(RoundedCornerShape(10.dp))
|
||||||
|
.background(if (isSelected) F7Colors.PrimaryLight else F7Colors.SurfaceMuted)
|
||||||
|
.then(
|
||||||
|
if (isSelected) {
|
||||||
|
Modifier.border(
|
||||||
|
1.dp,
|
||||||
|
F7Colors.Primary.copy(alpha = 0.25f),
|
||||||
|
RoundedCornerShape(10.dp),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.clickable { onSelect(option) }
|
||||||
|
.padding(horizontal = 10.dp, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
label(option),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = F7Colors.TextPrimary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CreateSheetScaffold(
|
||||||
|
title: String,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.94f)
|
||||||
|
.heightIn(max = 620.dp),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
color = Color.White,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
) {
|
||||||
|
// Живая тема: заголовки модалок 18/600
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style = MaterialTheme.typography.titleMedium.copy(fontSize = 18.sp),
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = F7Colors.TextPrimary,
|
||||||
|
)
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun DueDateRow(dueDate: LocalDate?, onChange: (LocalDate?) -> Unit) {
|
||||||
|
var pickerOpen by remember { mutableStateOf(false) }
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 36.dp)
|
||||||
|
.clip(RoundedCornerShape(10.dp))
|
||||||
|
.background(F7Colors.SurfaceMuted)
|
||||||
|
.clickable { pickerOpen = true }
|
||||||
|
.padding(horizontal = 10.dp, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
dueDate?.format(DateTimeFormatter.ofPattern("d MMMM yyyy"))
|
||||||
|
?: "Срок не задан (нажмите, чтобы выбрать)",
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = if (dueDate != null) F7Colors.TextPrimary else F7Colors.TextSecondary,
|
||||||
|
)
|
||||||
|
if (dueDate != null) {
|
||||||
|
Text(
|
||||||
|
"Сбросить",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = F7Colors.Primary,
|
||||||
|
modifier = Modifier.clickable { onChange(null) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pickerOpen) {
|
||||||
|
val state = rememberDatePickerState(
|
||||||
|
initialSelectedDateMillis = (dueDate ?: LocalDate.now())
|
||||||
|
.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli(),
|
||||||
|
)
|
||||||
|
DatePickerDialog(
|
||||||
|
onDismissRequest = { pickerOpen = false },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
state.selectedDateMillis?.let { millis ->
|
||||||
|
onChange(
|
||||||
|
Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault()).toLocalDate(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
pickerOpen = false
|
||||||
|
}) { Text("ОК", color = F7Colors.Primary) }
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { pickerOpen = false }) {
|
||||||
|
Text("Отмена", color = F7Colors.TextSecondary)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
DatePicker(state = state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Модалка «Создать задачу» из письма (паритет живого веба). */
|
||||||
|
@Composable
|
||||||
|
fun MailCreateTaskSheet(
|
||||||
|
visible: Boolean,
|
||||||
|
taskLists: List<MailTargetList>,
|
||||||
|
defaultSummary: String,
|
||||||
|
defaultDescription: String,
|
||||||
|
creating: Boolean,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onCreate: (listHref: String, summary: String, description: String, due: LocalDate?) -> Unit,
|
||||||
|
) {
|
||||||
|
if (!visible) return
|
||||||
|
var summary by rememberSaveable(defaultSummary) { mutableStateOf(defaultSummary) }
|
||||||
|
var description by rememberSaveable(defaultDescription) { mutableStateOf(defaultDescription) }
|
||||||
|
var dueDate by remember { mutableStateOf<LocalDate?>(null) }
|
||||||
|
var selectedList by remember(taskLists) { mutableStateOf(taskLists.firstOrNull()) }
|
||||||
|
|
||||||
|
CreateSheetScaffold(title = "Создать задачу", onDismiss = onDismiss) {
|
||||||
|
F7OutlinedField(value = summary, onValueChange = { summary = it }, label = "Название")
|
||||||
|
F7OutlinedField(
|
||||||
|
value = description,
|
||||||
|
onValueChange = { description = it },
|
||||||
|
label = "Описание",
|
||||||
|
minLines = 3,
|
||||||
|
)
|
||||||
|
if (taskLists.isNotEmpty()) {
|
||||||
|
Text("Список задач", style = MaterialTheme.typography.bodyMedium, color = F7Colors.TextSecondary)
|
||||||
|
PickerRows(
|
||||||
|
options = taskLists,
|
||||||
|
selected = selectedList,
|
||||||
|
label = { it.name },
|
||||||
|
onSelect = { selectedList = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text("Срок", style = MaterialTheme.typography.bodyMedium, color = F7Colors.TextSecondary)
|
||||||
|
DueDateRow(dueDate) { dueDate = it }
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
F7SecondaryButton("Отмена", onClick = onDismiss, modifier = Modifier.weight(1f))
|
||||||
|
F7PrimaryButton(
|
||||||
|
text = if (creating) "Создание…" else "Создать",
|
||||||
|
onClick = {
|
||||||
|
selectedList?.let { onCreate(it.href, summary.trim(), description.trim(), dueDate) }
|
||||||
|
},
|
||||||
|
enabled = !creating && summary.isNotBlank() && selectedList != null,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Модалка «Создать событие» из письма (паритет живого веба). */
|
||||||
|
@Composable
|
||||||
|
fun MailCreateEventSheet(
|
||||||
|
visible: Boolean,
|
||||||
|
calendars: List<MailTargetList>,
|
||||||
|
defaultTitle: String,
|
||||||
|
defaultDescription: String,
|
||||||
|
creating: Boolean,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onCreate: (
|
||||||
|
calendarHref: String,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
date: LocalDate,
|
||||||
|
startTime: String,
|
||||||
|
endTime: String,
|
||||||
|
allDay: Boolean,
|
||||||
|
) -> Unit,
|
||||||
|
) {
|
||||||
|
if (!visible) return
|
||||||
|
var title by rememberSaveable(defaultTitle) { mutableStateOf(defaultTitle) }
|
||||||
|
var description by rememberSaveable(defaultDescription) { mutableStateOf(defaultDescription) }
|
||||||
|
var date by remember { mutableStateOf(LocalDate.now()) }
|
||||||
|
var startTime by rememberSaveable { mutableStateOf("10:00") }
|
||||||
|
var endTime by rememberSaveable { mutableStateOf("11:00") }
|
||||||
|
var allDay by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var selectedCalendar by remember(calendars) { mutableStateOf(calendars.firstOrNull()) }
|
||||||
|
|
||||||
|
CreateSheetScaffold(title = "Создать событие", onDismiss = onDismiss) {
|
||||||
|
F7OutlinedField(value = title, onValueChange = { title = it }, label = "Название")
|
||||||
|
Text("Дата", style = MaterialTheme.typography.bodyMedium, color = F7Colors.TextSecondary)
|
||||||
|
DueDateRow(date) { picked -> picked?.let { date = it } }
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = allDay,
|
||||||
|
onCheckedChange = { allDay = it },
|
||||||
|
colors = CheckboxDefaults.colors(checkedColor = F7Colors.Primary),
|
||||||
|
)
|
||||||
|
Text("Весь день", style = MaterialTheme.typography.bodyMedium, color = F7Colors.TextPrimary)
|
||||||
|
}
|
||||||
|
if (!allDay) {
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
|
F7OutlinedField(value = startTime, onValueChange = { startTime = it }, label = "Начало (ЧЧ:ММ)")
|
||||||
|
}
|
||||||
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
|
F7OutlinedField(value = endTime, onValueChange = { endTime = it }, label = "Конец (ЧЧ:ММ)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (calendars.isNotEmpty()) {
|
||||||
|
Text("Календарь", style = MaterialTheme.typography.bodyMedium, color = F7Colors.TextSecondary)
|
||||||
|
PickerRows(
|
||||||
|
options = calendars,
|
||||||
|
selected = selectedCalendar,
|
||||||
|
label = { it.name },
|
||||||
|
onSelect = { selectedCalendar = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
F7OutlinedField(
|
||||||
|
value = description,
|
||||||
|
onValueChange = { description = it },
|
||||||
|
label = "Описание",
|
||||||
|
minLines = 3,
|
||||||
|
)
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
F7SecondaryButton("Отмена", onClick = onDismiss, modifier = Modifier.weight(1f))
|
||||||
|
F7PrimaryButton(
|
||||||
|
text = if (creating) "Создание…" else "Создать",
|
||||||
|
onClick = {
|
||||||
|
selectedCalendar?.let {
|
||||||
|
onCreate(it.href, title.trim(), description.trim(), date, startTime, endTime, allDay)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = !creating && title.isNotBlank() && selectedCalendar != null,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Цель создания (список задач или календарь): href + имя. */
|
||||||
|
data class MailTargetList(val href: String, val name: String)
|
||||||
@@ -465,6 +465,34 @@ fun MailScreen(
|
|||||||
openScheduledReplyForMessage(detail, option)
|
openScheduledReplyForMessage(detail, option)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
// «Создать задачу/событие из письма» — паритет живого веба:
|
||||||
|
// префилл темой письма и отправителем.
|
||||||
|
val createDefaults = state.messageDetail?.let { detail ->
|
||||||
|
detail.subject.ifBlank { "(без темы)" } to
|
||||||
|
"Из письма от ${detail.from.ifBlank { detail.fromEmail }}"
|
||||||
|
}
|
||||||
|
MailCreateTaskSheet(
|
||||||
|
visible = state.createTaskSheetOpen,
|
||||||
|
taskLists = state.createTargets,
|
||||||
|
defaultSummary = createDefaults?.first.orEmpty(),
|
||||||
|
defaultDescription = createDefaults?.second.orEmpty(),
|
||||||
|
creating = state.creatingFromMessage,
|
||||||
|
onDismiss = vm::dismissCreateSheets,
|
||||||
|
onCreate = { listHref, summary, description, due ->
|
||||||
|
vm.createTaskFromMessage(session, listHref, summary, description, due)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
MailCreateEventSheet(
|
||||||
|
visible = state.createEventSheetOpen,
|
||||||
|
calendars = state.createTargets,
|
||||||
|
defaultTitle = createDefaults?.first.orEmpty(),
|
||||||
|
defaultDescription = createDefaults?.second.orEmpty(),
|
||||||
|
creating = state.creatingFromMessage,
|
||||||
|
onDismiss = vm::dismissCreateSheets,
|
||||||
|
onCreate = { calHref, title, description, date, start, end, allDay ->
|
||||||
|
vm.createEventFromMessage(session, calHref, title, description, date, start, end, allDay)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
MailInboxScreen(
|
MailInboxScreen(
|
||||||
@@ -783,6 +811,8 @@ private fun MailMessageDetailHeader(
|
|||||||
onEditTags = onTagsSheetOpen,
|
onEditTags = onTagsSheetOpen,
|
||||||
onMove = onMoveSheetOpen,
|
onMove = onMoveSheetOpen,
|
||||||
onSnooze = onSnoozeSheetOpen,
|
onSnooze = onSnoozeSheetOpen,
|
||||||
|
onCreateTask = { vm.openCreateTaskSheet(session) },
|
||||||
|
onCreateEvent = { vm.openCreateEventSheet(session) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import ru.forbion.f7cloud.core.auth.AuthSession
|
import ru.forbion.f7cloud.core.auth.AuthSession
|
||||||
@@ -44,6 +45,11 @@ data class MailUiState(
|
|||||||
val appSettingsSaving: Boolean = false,
|
val appSettingsSaving: Boolean = false,
|
||||||
val error: String? = null,
|
val error: String? = null,
|
||||||
val unauthorized: Boolean = false,
|
val unauthorized: Boolean = false,
|
||||||
|
// «Создать задачу/событие из письма» (паритет живого веба)
|
||||||
|
val createTaskSheetOpen: Boolean = false,
|
||||||
|
val createEventSheetOpen: Boolean = false,
|
||||||
|
val createTargets: List<MailTargetList> = emptyList(),
|
||||||
|
val creatingFromMessage: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
class MailViewModel(
|
class MailViewModel(
|
||||||
@@ -995,6 +1001,104 @@ class MailViewModel(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- «Создать задачу/событие из письма» (паритет живого веба) ---
|
||||||
|
|
||||||
|
fun openCreateTaskSheet(session: AuthSession) {
|
||||||
|
_state.update { it.copy(createTaskSheetOpen = true, createTargets = emptyList()) }
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
ru.forbion.f7cloud.feature.tasks.TasksRepository()
|
||||||
|
.listTaskLists(session)
|
||||||
|
.map { MailTargetList(it.href, it.displayName) }
|
||||||
|
}.onSuccess { lists ->
|
||||||
|
_state.update { it.copy(createTargets = lists) }
|
||||||
|
}.onFailure { t ->
|
||||||
|
_state.update { it.copy(createTaskSheetOpen = false, error = t.message) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openCreateEventSheet(session: AuthSession) {
|
||||||
|
_state.update { it.copy(createEventSheetOpen = true, createTargets = emptyList()) }
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
ru.forbion.f7cloud.feature.calendar.CalendarRepository()
|
||||||
|
.listCalendars(session)
|
||||||
|
.map { MailTargetList(it.href, it.displayName) }
|
||||||
|
}.onSuccess { calendars ->
|
||||||
|
_state.update { it.copy(createTargets = calendars) }
|
||||||
|
}.onFailure { t ->
|
||||||
|
_state.update { it.copy(createEventSheetOpen = false, error = t.message) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissCreateSheets() {
|
||||||
|
_state.update { it.copy(createTaskSheetOpen = false, createEventSheetOpen = false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createTaskFromMessage(
|
||||||
|
session: AuthSession,
|
||||||
|
listHref: String,
|
||||||
|
summary: String,
|
||||||
|
description: String,
|
||||||
|
due: java.time.LocalDate?,
|
||||||
|
) {
|
||||||
|
_state.update { it.copy(creatingFromMessage = true) }
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
ru.forbion.f7cloud.feature.tasks.TasksRepository().createTask(
|
||||||
|
session = session,
|
||||||
|
listHref = listHref,
|
||||||
|
summary = summary,
|
||||||
|
dueDate = due,
|
||||||
|
description = description,
|
||||||
|
)
|
||||||
|
}.onSuccess {
|
||||||
|
_state.update {
|
||||||
|
it.copy(creatingFromMessage = false, createTaskSheetOpen = false, error = null)
|
||||||
|
}
|
||||||
|
}.onFailure { t ->
|
||||||
|
_state.update { it.copy(creatingFromMessage = false, error = t.message) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createEventFromMessage(
|
||||||
|
session: AuthSession,
|
||||||
|
calendarHref: String,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
date: java.time.LocalDate,
|
||||||
|
startTime: String,
|
||||||
|
endTime: String,
|
||||||
|
allDay: Boolean,
|
||||||
|
) {
|
||||||
|
_state.update { it.copy(creatingFromMessage = true) }
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
ru.forbion.f7cloud.feature.calendar.CalendarRepository().saveEvent(
|
||||||
|
session,
|
||||||
|
ru.forbion.f7cloud.feature.calendar.CalendarEventDraft(
|
||||||
|
title = title,
|
||||||
|
date = date,
|
||||||
|
startTime = startTime,
|
||||||
|
endTime = endTime,
|
||||||
|
allDay = allDay,
|
||||||
|
description = description,
|
||||||
|
calendarHref = calendarHref,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}.onSuccess {
|
||||||
|
_state.update {
|
||||||
|
it.copy(creatingFromMessage = false, createEventSheetOpen = false, error = null)
|
||||||
|
}
|
||||||
|
}.onFailure { t ->
|
||||||
|
_state.update { it.copy(creatingFromMessage = false, error = t.message) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val MARK_READ_DELAY_MS = 2000L
|
private const val MARK_READ_DELAY_MS = 2000L
|
||||||
private const val FOLDER_CACHE_TTL_MS = 15 * 60 * 1000L
|
private const val FOLDER_CACHE_TTL_MS = 15 * 60 * 1000L
|
||||||
|
|||||||
Reference in New Issue
Block a user