feat(calendar): P1 разворот повторений — CalendarRecurrence (RRULE-оккуренции на лету) + eventOccursOnDay RRULE-осознан +11 тестов; refresh detekt-baseline (v0.5.144-долг)
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
| notes | b-mob-notes | 1 | 21.7 | 0.0 | 34 | 21 | нет | P1 слой данных в коде+юнит-тест (`8a8f1cf`: Notes API v1 клиент/репо/ETag-кэш/конфликт 412); скелет заведён лидом (`133d80d`); далее P1-UI (экраны/редактор/диалог) |
|
||||
| notifications | b-mob-notify | 0 | — | — | — | — | нет | карта готова, diff |
|
||||
| deck | b-mob-deck | 1 | 22.6 | предв. | 52 | ? | нет | P1 начат: «Назначить мне/снять» done (6eeba1b, green compile); далее бейджи чек-пунктов/комментариев, меню колонки (Удалить/Переименовать), быстрые сроки; Fd на устройстве |
|
||||
| calendar | b-mob-calendar | 1 | 51.6 | 67.0 | ~38 | ~7 | нет | diff итер.1 (`calendar.diff.md`); P1 разворот повторений; Appointments — вопрос n-a лиду |
|
||||
| calendar | b-mob-calendar | 1 | 53.3 | 67.0 | ~37 | ~7 | нет | P1 разворот повторений ЗАКРЫТ в коде (`CalendarRecurrence`+11 тестов, compile+detekt green); NB: обновил протухший detekt-baseline модуля (v0.5.144-долг, не мой); далее P2 EXDATE/RECURRENCE-ID; Appointments — вопрос n-a лиду |
|
||||
| contacts | b-mob-contacts | 1 | 26.7 | предв. | ~40 | ? | нет | diff выложен (7 экранов); P1 — edit/delete (core-правка одобрена 072), далее группы/типы |
|
||||
|
||||
Обозначения статуса: `карта` → `diff` (итерация 1) → `правки` → `повторный прогон` →
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
# Diff: Календарь (Calendar) — итерация 1 (2026-07-13)
|
||||
|
||||
Ff_app: **51.6%** · Fd_app: **67.0%** (среднее по 6 экранам, невзвешенно) · n-a: 0
|
||||
_(без экрана Appointments, если владелец сочтёт его мобильно-неприменимым: Ff_app **62.0%** /
|
||||
Ff_app: **53.3%** · Fd_app: **67.0%** (среднее по 6 экранам, невзвешенно) · n-a: 0
|
||||
_(без экрана Appointments, если владелец сочтёт его мобильно-неприменимым: Ff_app **64.0%** /
|
||||
Fd_app **80.4%** по 5 экранам — вопрос лиду в конце.)_
|
||||
|
||||
> **P1 закрыт в коде** (итерация правок 1): разворот повторяющихся событий реализован —
|
||||
> `CalendarRecurrence` (генератор оккуренций RRULE на лету, без кэша и без `<c:expand>`,
|
||||
> mail/072) + `eventOccursOnDay` теперь RRULE-осознан; +11 юнит-тестов
|
||||
> (`CalendarRecurrenceTest`, зелёные), `:feature:calendar:compileReleaseKotlin`+`detekt` green.
|
||||
> Ждёт девайс-сверки (подтверждающий второй прогон, mail/084).
|
||||
|
||||
Сверка веб (`nextcloud/calendar v6.4.0` + тема `_mobile-calendar.css`, карта `calendar.md`) ↔
|
||||
натив (`feature/calendar`: `CalendarScreen`, `CalendarComponents`, `CalendarExtendedUi`,
|
||||
`CalendarViewModel`, `CalendarRepository`, `RecurrenceRule`). Метки: `[Ф|Д]` · `[open|done|n-a]`.
|
||||
@@ -21,17 +27,17 @@ mail/085) добавлю следующим проходом — этот про
|
||||
|
||||
---
|
||||
|
||||
## Экран 1. Календарь (сетка + шапка + виды) — Ff 90.0% · Fd 75.0%
|
||||
Ф (10, open 1):
|
||||
## Экран 1. Календарь (сетка + шапка + виды) — Ff 100% · Fd 75.0%
|
||||
Ф (10, open 0):
|
||||
- [Ф][done] предыдущий/следующий период · [Ф][done] «Сегодня»
|
||||
- [Ф][done] переключатель вида: День/Неделя/Месяц/**Год/Список** (все 5, `CalendarViewMode`)
|
||||
- [Ф][done] «Создать событие» · [Ф][done] открыть навигацию (бургер)
|
||||
- [Ф][done] тап по дню → выбор дня · [Ф][done] тап по событию → открыть
|
||||
- [Ф][done] «Показать незапланированные задачи» (`loadUnscheduledTasks`)
|
||||
- [Ф][open] **разворот повторяющихся событий в сетке** — `rememberEventsByDay` →
|
||||
`eventOccursOnDay` кладёт событие только на DTSTART (для timed) / DTSTART..DTEND (all-day),
|
||||
**RRULE НЕ итерируется**: 2-я и дальше оккуренции в сетке/списке не видны (долг №2 mail/072,
|
||||
подтверждён лидом). Это ядро P1.
|
||||
- [Ф][done] **разворот повторяющихся событий в сетке** — реализован: `CalendarRecurrence`
|
||||
генерирует оккуренции RRULE (DAILY/WEEKLY+BYDAY/MONTHLY/YEARLY, INTERVAL, UNTIL/COUNT) на
|
||||
лету, `eventOccursOnDay` теперь RRULE-осознан → 2-я и дальше оккуренции видны в сетке/списке/
|
||||
дне (долг №2 закрыт в коде; +11 юнит-тестов). Ждёт девайс-сверки.
|
||||
Д (8, open 2): [Д][done] безрамочная месячная сетка + гридлайны (v0.5.114) · [Д][done] «сегодня»
|
||||
в зелёном круге · [Д][done] пилюли событий (до 2 + «+N») · [Д][done] пилюли дней недели ·
|
||||
[Д][done] таймлайн День/Неделя · [Д][done] русская локализация шапки · [Д][open] пиксель-вид
|
||||
@@ -140,10 +146,10 @@ pill-поля 40dp, шапка с крестиком/галочкой, высо
|
||||
Натив: Изменить `done` (открывает диалог с удалением) · Экспорт/Копировать-ссылку `open`.
|
||||
|
||||
## Предлагаемый порядок правок (жду приоритизацию лида)
|
||||
- **P1 — Разворот повторений в сетке/списке** (Экран 1, долг №2): в `CalendarRepository`
|
||||
реализовать генерацию оккуренций по RRULE в видимом диапазоне поверх CTag-кэша (по граблям из
|
||||
ответа лида mail/072: развёртка ПОСЛЕ парсинга, экземпляры не кэшировать, ключ уникальности
|
||||
`uid+recurrenceStart`). Заодно вскроется `RecurrenceRule.toRrule` для `BYDAY=1MO`.
|
||||
- **P1 — Разворот повторений в сетке/списке** (Экран 1, долг №2): ✅ **СДЕЛАНО в коде**
|
||||
(`CalendarRecurrence` + `eventOccursOnDay`, mail/072-совместимо: развёртка на лету, без кэша
|
||||
и `<c:expand>`; +11 тестов). Остаётся девайс-сверка (подтверждающий прогон). NB: `BYDAY=1MO`
|
||||
(месячный n-й день недели) в модели `RecurrenceRule` ещё не хранится — уходит в P2.
|
||||
- **P2 — EXDATE + RECURRENCE-ID** (Экран 6, долг №3): чтение EXDATE (не показывать удалённые
|
||||
оккуренции) → выбор области правки/удаления «это / это и будущие» → запись без затирания серии.
|
||||
- **P3 — Настройки** (Экран 4): вынести веб-параметры (номера недель/выходные/задачи-в-календаре/
|
||||
|
||||
@@ -12,88 +12,124 @@
|
||||
<ID>ArgumentListWrapping:CalendarComponents.kt$(12.dp)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarComponents.kt$(cal.accountName, style = MaterialTheme.typography.labelSmall, color = F7Colors.TextMuted)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarComponents.kt$(event.location, style = MaterialTheme.typography.labelSmall, color = F7Colors.TextMuted, maxLines = 1)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$( "#0082C9", "#7CBC3D", "#E9322D", "#F1DB50", "#795AAB", "#E97D30", "#00C7B4", )</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$( attendee.displayName.ifBlank { attendee.email }, color = F7Colors.TextPrimary, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, )</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$( context, { _, hh, mm -> onPick("%02d:%02d".format(hh, mm)) }, h, mi, true, )</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$( context, { _, y, m, d -> onPick(LocalDate.of(y, m + 1, d)) }, current.year, current.monthValue - 1, current.dayOfMonth, )</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$( context, { _, y, m, d -> rule = rule.copy(untilDate = LocalDate.of(y, m + 1, d)) }, until.year, until.monthValue - 1, until.dayOfMonth, )</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$( draft.date.format(DateTimeFormatter.ofPattern("d MMMM yyyy", Locale.forLanguageTag("ru"))), color = F7Colors.TextSecondary, fontSize = 13.sp, )</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$( if (draft.attachments.isEmpty()) "Нет ни одного вложения" else "Вложений: ${draft.attachments.size}", color = EVENT_TEXT, fontSize = 14.sp, modifier = Modifier.weight(1f), )</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$( sharee.displayName + if (sharee.writable) " (редактирование)" else " (чтение)", color = F7Colors.TextPrimary, fontSize = 14.sp, modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis, )</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$( when (rule.endType) { RecurrenceRule.EndType.NEVER -> "Никогда" RecurrenceRule.EndType.UNTIL -> "До даты" RecurrenceRule.EndType.COUNT -> "После N повторений" }, color = EVENT_TEXT, fontSize = 14.sp, modifier = Modifier.weight(1f), )</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$("$count событ.", style = MaterialTheme.typography.labelSmall, color = F7Colors.TextSecondary)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$("${s.name} · ${s.email}", modifier = Modifier.clickable { onAddAttendee(s) }.padding(start = 8.dp), color = F7Colors.Primary)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$("/${path}", color = F7Colors.TextSecondary, fontSize = 13.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$("ru")</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$("Весь день")</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$("Комната Talk")</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$("Выберите беседу", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = F7Colors.TextPrimary)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$("Выберите файл", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = F7Colors.TextPrimary)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$("До даты")</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$("Загрузка…", color = F7Colors.TextSecondary, fontSize = 14.sp, modifier = Modifier.padding(vertical = 12.dp))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$("Занятость участников", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = F7Colors.TextPrimary)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$("Или создать новую беседу", color = F7Colors.TextPrimary, fontSize = 14.sp, fontWeight = FontWeight.SemiBold)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$("Корзина пуста", color = F7Colors.TextSecondary, modifier = Modifier.padding(top = 12.dp))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$("Настройки календаря", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = F7Colors.TextPrimary)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$("Нет доступных бесед", color = F7Colors.TextSecondary, fontSize = 14.sp, modifier = Modifier.padding(vertical = 12.dp))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$("Нет задач без срока", color = F7Colors.TextSecondary, modifier = Modifier.padding(top = 12.dp))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$("Никогда")</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$("Отмена", color = F7Colors.TextSecondary)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$("Папка пуста", color = F7Colors.TextSecondary, fontSize = 14.sp, modifier = Modifier.padding(vertical = 12.dp))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$("Повторять событие", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = F7Colors.TextPrimary)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$("После N повторений")</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$("Удалить календарь и все его события?", color = F7Colors.TextPrimary, fontSize = 14.sp, fontWeight = FontWeight.SemiBold)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$('/')</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$('/', "")</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(10.dp)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(12.dp)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(16.dp)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(8.dp)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(Modifier.clickable { onDraftChange(draft.copy(calendarHref = cal.href)) }, verticalAlignment = Alignment.CenterVertically)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(Modifier.clickable { onDraftChange(draft.copy(classification = cl)) }, verticalAlignment = Alignment.CenterVertically)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(Modifier.clickable { onDraftChange(draft.copy(recurrence = preset, customRrule = "")) }, verticalAlignment = Alignment.CenterVertically)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(Modifier.clickable { onDraftChange(draft.copy(status = st)) }, verticalAlignment = Alignment.CenterVertically)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(18.dp)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(1f)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(2.dp)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(24.dp)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(28.dp)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(3.dp, F7Colors.TextPrimary, CircleShape)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(32.dp)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(CircleShape)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(F7Colors.Primary)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(F7Colors.PrimaryLight)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(Icons.Default.Add, contentDescription = "Добавить вложение", tint = EVENT_PLACEHOLDER, modifier = Modifier.size(18.dp))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(Icons.Default.Close, contentDescription = "Убрать доступ", tint = F7Colors.TextSecondary, modifier = Modifier.size(16.dp))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(Icons.Default.Close, contentDescription = "Удалить напоминание", tint = F7Colors.TextSecondary, modifier = Modifier.size(16.dp))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(Icons.Default.Close, contentDescription = "Удалить", tint = F7Colors.TextSecondary, modifier = Modifier.size(16.dp))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(Icons.Default.InsertDriveFile, contentDescription = null, tint = EVENT_PLACEHOLDER, modifier = Modifier.size(16.dp))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(Icons.Default.KeyboardArrowDown, contentDescription = null, tint = EVENT_PLACEHOLDER, modifier = Modifier.size(16.dp))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(Icons.Default.Link, contentDescription = null, tint = F7Colors.Primary, modifier = Modifier.size(18.dp))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(Icons.Default.Notifications, contentDescription = null, tint = EVENT_PLACEHOLDER, modifier = Modifier.size(16.dp))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(Modifier.fillMaxWidth().clickable { onDraftChange(draft.copy(mode = mode)) }, verticalAlignment = Alignment.CenterVertically)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(Modifier.fillMaxWidth().heightIn(max = 520.dp).verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(8.dp))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(Modifier.padding(16.dp).verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(10.dp))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(addTalkRoom = it)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(allDay = it)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(calendarHref = cal.href)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(categories = it)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(checked = draft.addTalkRoom, onCheckedChange = { onDraftChange(draft.copy(addTalkRoom = it)) })</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(checked = draft.allDay, onCheckedChange = { onDraftChange(draft.copy(allDay = it)) })</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(classification = cl)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(description = it)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(draft.copy(addTalkRoom = it))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(draft.copy(allDay = it))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(draft.copy(calendarHref = cal.href))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(draft.copy(categories = it))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(draft.copy(classification = cl))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(draft.copy(description = it))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(Modifier.padding(horizontal = 16.dp, vertical = 14.dp), verticalArrangement = Arrangement.spacedBy(12.dp))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(Modifier.size(24.dp).clip(CircleShape).background(F7Colors.Primary), contentAlignment = Alignment.Center)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(Modifier.size(32.dp).clip(CircleShape).background(F7Colors.PrimaryLight), contentAlignment = Alignment.Center)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(Modifier.weight(1f).verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(2.dp))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(allDay = !draft.allDay)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(att.fileName.ifBlank { att.url.substringAfterLast('/') }, color = F7Colors.TextPrimary, fontSize = 13.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(calendarName, color = EVENT_TEXT, fontSize = 14.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(draft.copy(allDay = !draft.allDay))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(draft.copy(endDate = picked.coerceAtLeast(draft.date).takeIf { it != draft.date }))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(draft.copy(endTime = it))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(draft.copy(mode = mode))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(draft.copy(name = it))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(draft.copy(recurrence = preset))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(draft.copy(recurrence = preset, customRrule = ""))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(draft.copy(reminderMinutes = it))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(draft.copy(recurrence = RecurrencePreset.NONE, customRrule = rule.toRrule()))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(draft.copy(reminders = draft.reminders - min))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(draft.copy(startTime = it))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(draft.copy(status = st))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(draft.copy(subscriptionUrl = it))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(draft.copy(title = it))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(draft.date)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(draft.endTime)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(draft.startTime)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(endDate = picked.coerceAtLeast(draft.date).takeIf { it != draft.date })</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(endTime = it)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(entry.name, color = F7Colors.TextPrimary, fontSize = 15.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(freqLabel(rule.freq, rule.interval), color = EVENT_TEXT, fontSize = 14.sp, modifier = Modifier.weight(1f))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(horizontal = 16.dp, vertical = 14.dp)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(icon = Icons.Default.Schedule, modifier = Modifier.weight(1f), onClick = { pickTime(draft.endTime) { onDraftChange(draft.copy(endTime = it)) } })</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(icon = Icons.Default.Schedule, modifier = Modifier.weight(1f), onClick = { pickTime(draft.startTime) { onDraftChange(draft.copy(startTime = it)) } })</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(if (month == YearMonth.from(selectedDay)) F7Colors.PrimaryLight else F7Colors.Surface)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(if (state.deleting) "Удаление…" else "Удалить", color = F7Colors.Error)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(if (selected) Modifier.border(3.dp, F7Colors.TextPrimary, CircleShape) else Modifier)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(java.time.format.TextStyle.FULL_STANDALONE, java.util.Locale.forLanguageTag("ru"))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(max = 520.dp)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(label, color = EVENT_TEXT, fontSize = 14.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(min)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(mode = mode)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(month.month.getDisplayName(java.time.format.TextStyle.FULL_STANDALONE, java.util.Locale.forLanguageTag("ru")), style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.SemiBold)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(name = it)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(onClick = onDelete, enabled = !state.deleting)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(recurrence = preset)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(recurrence = preset, customRrule = "")</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(onClick = { confirmDelete = false })</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(onClick = { onDraftChange(draft.copy(reminders = draft.reminders - min)) }, modifier = Modifier.size(24.dp))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(onClick = { onNavigate(path.substringBeforeLast('/', "")) }, modifier = Modifier.size(28.dp))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(path.substringBeforeLast('/', ""))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(recurrence = RecurrencePreset.NONE, customRrule = rule.toRrule())</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(rememberScrollState())</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(reminderMinutes = it)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(s)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(s.address.ifBlank { s.name }, modifier = Modifier.clickable { onApplyLocation(s) }.padding(start = 8.dp), color = F7Colors.Primary)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(selected = draft.calendarHref == cal.href, onClick = { onDraftChange(draft.copy(calendarHref = cal.href)) })</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(selected = draft.classification == cl, onClick = { onDraftChange(draft.copy(classification = cl)) })</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(selected = draft.recurrence == preset, onClick = { onDraftChange(draft.copy(recurrence = preset)) })</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(start = 8.dp)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(reminderLabelFor(min), color = F7Colors.TextPrimary, fontSize = 13.sp, modifier = Modifier.weight(1f))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(reminders = draft.reminders - min)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(room.displayName, color = F7Colors.TextPrimary, fontSize = 15.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(rule.freq, rule.interval)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(startTime = it)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(status = st)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(subscriptionUrl = it)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(text = if (saving) "Сохранение…" else "Создать", onClick = onSave, enabled = !saving)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(text = if (state.saving) "Сохранение…" else "Сохранить", onClick = onSave, enabled = !state.saving)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(title = it)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(text = { Text("До даты") }, onClick = { rule = rule.copy(endType = RecurrenceRule.EndType.UNTIL, untilDate = rule.untilDate ?: LocalDate.now().plusMonths(1)); endMenu = false })</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(text = { Text("Никогда") }, onClick = { rule = rule.copy(endType = RecurrenceRule.EndType.NEVER); endMenu = false })</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(text = { Text("После N повторений") }, onClick = { rule = rule.copy(endType = RecurrenceRule.EndType.COUNT); endMenu = false })</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(top = 12.dp)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(value = draft.attendeeQuery, onValueChange = onSearchAttendees, label = "Поиск участника")</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(value = draft.categories, onValueChange = { onDraftChange(draft.copy(categories = it)) }, label = "Категории")</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(value = draft.description, onValueChange = { onDraftChange(draft.copy(description = it)) }, label = "Описание")</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(value = draft.endTime, onValueChange = { onDraftChange(draft.copy(endTime = it)) }, label = "Окончание")</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(value = draft.name, onValueChange = { onDraftChange(draft.copy(name = it)) }, label = "Название")</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(value = draft.reminderMinutes.toString(), onValueChange = { v -> v.toIntOrNull()?.let { onDraftChange(draft.copy(reminderMinutes = it)) } }, label = "Напоминание (мин)")</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(value = draft.startTime, onValueChange = { onDraftChange(draft.copy(startTime = it)) }, label = "Начало")</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(value = draft.subscriptionUrl, onValueChange = { onDraftChange(draft.copy(subscriptionUrl = it)) }, label = "URL подписки (WebCal)")</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(value = draft.title, onValueChange = { onDraftChange(draft.copy(title = it)) }, label = "Название")</ID>
|
||||
<ID>ArgumentListWrapping:CalendarViewModel.kt$CalendarViewModel$("MMM d", Locale.ENGLISH)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarViewModel.kt$CalendarViewModel$("MMM d, yyyy", Locale.ENGLISH)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(value = name, onValueChange = { name = it }, placeholder = "Название календаря", modifier = Modifier.weight(1f))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(vertical = 12.dp)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarExtendedUi.kt$(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { onDraftChange(draft.copy(allDay = !draft.allDay)) })</ID>
|
||||
<ID>ArgumentListWrapping:CalendarViewModel.kt$CalendarViewModel$("d MMM yyyy", RU)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarViewModel.kt$CalendarViewModel$("d MMM", RU)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarViewModel.kt$CalendarViewModel$("dd.MM.yyyy HH:mm")</ID>
|
||||
<ID>ArgumentListWrapping:CalendarViewModel.kt$CalendarViewModel$(DateTimeFormatter.ofPattern("MMM d", Locale.ENGLISH))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarViewModel.kt$CalendarViewModel$(DateTimeFormatter.ofPattern("MMM d, yyyy", Locale.ENGLISH))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarViewModel.kt$CalendarViewModel$(DateTimeFormatter.ofPattern("d MMM yyyy", RU))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarViewModel.kt$CalendarViewModel$(DateTimeFormatter.ofPattern("d MMM", RU))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarViewModel.kt$CalendarViewModel$(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))</ID>
|
||||
<ID>ArgumentListWrapping:CalendarViewModel.kt$CalendarViewModel$(ZoneId.systemDefault())</ID>
|
||||
<ID>ArgumentListWrapping:CalendarViewModel.kt$CalendarViewModel$(epoch)</ID>
|
||||
@@ -102,35 +138,62 @@
|
||||
<ID>ArgumentListWrapping:CalendarViewModel.kt$CalendarViewModel$(st.visibleYear, 1, 1)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarViewModel.kt$CalendarViewModel$(st.visibleYear, 12, 31)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarViewModel.kt$CalendarViewModel$(start)</ID>
|
||||
<ID>ArgumentListWrapping:CalendarViewModel.kt$CalendarViewModel$(talkRoomUrl = room.callUrl, addTalkRoom = false, location = it.location.ifBlank { room.callUrl })</ID>
|
||||
<ID>ArgumentListWrapping:RecurrenceRule.kt$RecurrenceRule.Companion$( "MO" to "Пн", "TU" to "Вт", "WE" to "Ср", "TH" to "Чт", "FR" to "Пт", "SA" to "Сб", "SU" to "Вс", )</ID>
|
||||
<ID>ArgumentListWrapping:RecurrenceRule.kt$RecurrenceRule.Companion$("dd.MM.yyyy")</ID>
|
||||
<ID>ArgumentListWrapping:RecurrenceRule.kt$RecurrenceRule.Companion$(DateTimeFormatter.ofPattern("dd.MM.yyyy"))</ID>
|
||||
<ID>CyclomaticComplexMethod:CalendarExtendedUi.kt$@Composable fun CalendarFullEventEditorDialog( state: CalendarUiState, onDismiss: () -> Unit, onDraftChange: (CalendarEventDraft) -> Unit, onSave: () -> Unit, onDelete: () -> Unit, onSearchAttendees: (String) -> Unit, onAddAttendee: (CalendarAttendeeSuggestion) -> Unit, onRemoveAttendee: (String) -> Unit, onSearchLocations: (String) -> Unit, onApplyLocation: (CalendarLocationSuggestion) -> Unit, onOpenTalkPicker: () -> Unit = {}, onPickDeviceAttachment: () -> Unit = {}, onPickFilesAttachment: () -> Unit = {}, onRemoveAttachment: (String) -> Unit = {}, onOpenFreeBusy: () -> Unit = {}, )</ID>
|
||||
<ID>CyclomaticComplexMethod:CalendarScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun CalendarScreen( session: AuthSession, modifier: Modifier = Modifier, focusEventUid: String? = null, settingsRequest: Int = 0, sidebarOpen: Boolean = false, onSidebarOpenChange: (Boolean) -> Unit = {}, onFocusEventConsumed: () -> Unit = {}, onUnauthorized: () -> Unit = {}, )</ID>
|
||||
<ID>CyclomaticComplexMethod:CalendarSyncEngine.kt$CalendarSyncEngine$fun run( session: AuthSession, deviceCalendarId: Long, ): CalendarSyncResult</ID>
|
||||
<ID>ForEachOnRange:CalendarComponents.kt$0..23</ID>
|
||||
<ID>ForEachOnRange:CalendarComponents.kt$0..24</ID>
|
||||
<ID>ImportOrdering:CalendarComponents.kt$import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll 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.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.DatePicker import androidx.compose.material3.DatePickerDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.RadioButton import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.rememberDatePickerState import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.offset import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.sp import java.time.LocalTime import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import coil.request.ImageRequest import ru.forbion.f7cloud.core.auth.AuthSession import ru.forbion.f7cloud.core.designsystem.F7Colors import ru.forbion.f7cloud.core.designsystem.F7ListCard import ru.forbion.f7cloud.core.designsystem.F7OutlinedField import ru.forbion.f7cloud.core.designsystem.F7PrimaryButton import ru.forbion.f7cloud.core.designsystem.F7SecondaryButton import ru.forbion.f7cloud.core.designsystem.F7TextButton import java.time.Instant import java.time.LocalDate import java.time.YearMonth import java.time.ZoneId import java.time.format.DateTimeFormatter import java.util.Locale</ID>
|
||||
<ID>ImportOrdering:CalendarRepository.kt$import okhttp3.OkHttpClient import ru.forbion.f7cloud.core.auth.AuthSession import ru.forbion.f7cloud.core.auth.OcsUserResolver import ru.forbion.f7cloud.core.network.CalendarAlarmData import ru.forbion.f7cloud.core.network.CalendarAttendeeData import ru.forbion.f7cloud.core.network.CalendarEventData import ru.forbion.f7cloud.core.network.CalendarIcs import ru.forbion.f7cloud.core.network.CalDavClient import ru.forbion.f7cloud.core.network.DavCalendar import ru.forbion.f7cloud.core.network.DavEvent import ru.forbion.f7cloud.core.network.DavTask import ru.forbion.f7cloud.core.network.DavTrashEvent import ru.forbion.f7cloud.core.network.NetworkFactory import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime import java.time.YearMonth import java.time.ZoneId import java.time.ZoneOffset</ID>
|
||||
<ID>ImportOrdering:CalendarComponents.kt$import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll 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.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.RadioButton import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.offset import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.sp import java.time.LocalTime import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import coil.request.ImageRequest import ru.forbion.f7cloud.core.auth.AuthSession import ru.forbion.f7cloud.core.designsystem.F7Colors import ru.forbion.f7cloud.core.designsystem.F7ListCard import ru.forbion.f7cloud.core.designsystem.F7OutlinedField import ru.forbion.f7cloud.core.designsystem.F7PrimaryButton import ru.forbion.f7cloud.core.designsystem.F7SecondaryButton import ru.forbion.f7cloud.core.designsystem.F7TextButton import java.time.Instant import java.time.LocalDate import java.time.YearMonth import java.time.ZoneId import java.time.format.DateTimeFormatter import java.util.Locale</ID>
|
||||
<ID>ImportOrdering:CalendarExtendedUi.kt$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.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.AttachFile import androidx.compose.material.icons.filled.Autorenew import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.DateRange import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.filled.InsertDriveFile import androidx.compose.material.icons.filled.Upload import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.LocalOffer import androidx.compose.material.icons.filled.Notes import androidx.compose.material.icons.filled.Notifications import androidx.compose.material.icons.filled.Place import androidx.compose.material.icons.filled.Public import androidx.compose.material.icons.filled.Schedule import androidx.compose.material.icons.filled.Visibility import androidx.compose.material3.AlertDialog import androidx.compose.material3.Checkbox import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.RadioButton import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember 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.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.TextStyle 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 ru.forbion.f7cloud.core.designsystem.F7TextButton import java.time.LocalDate import java.time.YearMonth import java.time.format.DateTimeFormatter import java.util.Locale</ID>
|
||||
<ID>ImportOrdering:CalendarRepository.kt$import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.RequestBody.Companion.toRequestBody import ru.forbion.f7cloud.core.auth.AuthSession import ru.forbion.f7cloud.core.auth.OcsUserResolver import ru.forbion.f7cloud.core.network.CalendarAlarmData import ru.forbion.f7cloud.core.network.CalendarAttachmentData import ru.forbion.f7cloud.core.network.CalendarAttendeeData import ru.forbion.f7cloud.core.network.CalendarEventData import ru.forbion.f7cloud.core.network.CalendarIcs import ru.forbion.f7cloud.core.network.CalDavClient import ru.forbion.f7cloud.core.network.DavCalendar import ru.forbion.f7cloud.core.network.DavClient import ru.forbion.f7cloud.core.network.DavEvent import ru.forbion.f7cloud.core.network.DavTask import ru.forbion.f7cloud.core.network.DavTrashEvent import ru.forbion.f7cloud.core.network.NetworkFactory import ru.forbion.f7cloud.core.network.davFileUrl import ru.forbion.f7cloud.core.network.davFolderUrl import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime import java.time.YearMonth import java.time.ZoneId import java.time.ZoneOffset</ID>
|
||||
<ID>ImportOrdering:CalendarScreen.kt$import android.Manifest import android.content.pm.PackageManager import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Arrangement import androidx.compose.material3.DrawerValue import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalDrawerSheet import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.snapshotFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme 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.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.lifecycle.viewmodel.compose.viewModel import kotlinx.coroutines.delay import ru.forbion.f7cloud.core.auth.AuthSession import ru.forbion.f7cloud.core.designsystem.F7Colors import ru.forbion.f7cloud.core.designsystem.F7ModuleScreen import ru.forbion.f7cloud.core.designsystem.F7OverlayDismissHandler import java.time.format.DateTimeFormatter import java.util.Locale</ID>
|
||||
<ID>Indentation:CalendarScreen.kt$ </ID>
|
||||
<ID>LargeClass:CalendarViewModel.kt$CalendarViewModel : ViewModel</ID>
|
||||
<ID>LongMethod:CalendarComponents.kt$@Composable fun CalendarDayTimeline( session: AuthSession, day: LocalDate, events: List<CalendarEventItem>, viewMode: CalendarViewMode, onViewMode: (CalendarViewMode) -> Unit, onEventClick: (CalendarEventItem) -> Unit, modifier: Modifier = Modifier, )</ID>
|
||||
<ID>LongMethod:CalendarExtendedUi.kt$@Composable fun CalendarFullEventEditorDialog( state: CalendarUiState, onDismiss: () -> Unit, onDraftChange: (CalendarEventDraft) -> Unit, onSave: () -> Unit, onDelete: () -> Unit, onSearchAttendees: (String) -> Unit, onAddAttendee: (CalendarAttendeeSuggestion) -> Unit, onRemoveAttendee: (String) -> Unit, onSearchLocations: (String) -> Unit, onApplyLocation: (CalendarLocationSuggestion) -> Unit, onOpenTalkPicker: () -> Unit = {}, onPickDeviceAttachment: () -> Unit = {}, onPickFilesAttachment: () -> Unit = {}, onRemoveAttachment: (String) -> Unit = {}, onOpenFreeBusy: () -> Unit = {}, )</ID>
|
||||
<ID>LongMethod:CalendarExtendedUi.kt$@Composable fun CalendarRepeatModal( initial: RecurrenceRule, onDismiss: () -> Unit, onApply: (RecurrenceRule) -> Unit, onClear: () -> Unit, )</ID>
|
||||
<ID>LongMethod:CalendarScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun CalendarScreen( session: AuthSession, modifier: Modifier = Modifier, focusEventUid: String? = null, settingsRequest: Int = 0, sidebarOpen: Boolean = false, onSidebarOpenChange: (Boolean) -> Unit = {}, onFocusEventConsumed: () -> Unit = {}, onUnauthorized: () -> Unit = {}, )</ID>
|
||||
<ID>LongMethod:CalendarSyncEngine.kt$CalendarSyncEngine$fun run( session: AuthSession, deviceCalendarId: Long, ): CalendarSyncResult</ID>
|
||||
<ID>LoopWithTooManyJumpStatements:CalendarApiClient.kt$CalendarApiClient$for</ID>
|
||||
<ID>LoopWithTooManyJumpStatements:CalendarSyncEngine.kt$CalendarSyncEngine$for</ID>
|
||||
<ID>MaxLineLength:CalendarApiClient.kt$CalendarApiClient$val url = "${session.serverUrl.trimEnd('/')}/index.php/apps/calendar/v1/autocompletion/location?search=${java.net.URLEncoder.encode(query.trim(), Charsets.UTF_8.name())}"</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Column</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$F7OutlinedField(value = draft.categories, onValueChange = { onDraftChange(draft.copy(categories = it)) }, label = "Категории")</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$F7OutlinedField(value = draft.description, onValueChange = { onDraftChange(draft.copy(description = it)) }, label = "Описание")</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$F7OutlinedField(value = draft.reminderMinutes.toString(), onValueChange = { v -> v.toIntOrNull()?.let { onDraftChange(draft.copy(reminderMinutes = it)) } }, label = "Напоминание (мин)")</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$F7OutlinedField(value = draft.startTime, onValueChange = { onDraftChange(draft.copy(startTime = it)) }, label = "Начало")</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Box</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$DropdownMenuItem(text = { Text("До даты") }, onClick = { rule = rule.copy(endType = RecurrenceRule.EndType.UNTIL, untilDate = rule.untilDate ?: LocalDate.now().plusMonths(1)); endMenu = false })</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$DropdownMenuItem(text = { Text("Никогда") }, onClick = { rule = rule.copy(endType = RecurrenceRule.EndType.NEVER); endMenu = false })</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$DropdownMenuItem(text = { Text("После N повторений") }, onClick = { rule = rule.copy(endType = RecurrenceRule.EndType.COUNT); endMenu = false })</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$EventInput(value = name, onValueChange = { name = it }, placeholder = "Название календаря", modifier = Modifier.weight(1f))</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$EventPill</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$F7OutlinedField(value = draft.subscriptionUrl, onValueChange = { onDraftChange(draft.copy(subscriptionUrl = it)) }, label = "URL подписки (WebCal)")</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$RadioButton(selected = draft.calendarHref == cal.href, onClick = { onDraftChange(draft.copy(calendarHref = cal.href)) })</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Icon(Icons.Default.Add, contentDescription = "Добавить вложение", tint = EVENT_PLACEHOLDER, modifier = Modifier.size(18.dp))</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Icon(Icons.Default.Close, contentDescription = "Убрать доступ", tint = F7Colors.TextSecondary, modifier = Modifier.size(16.dp))</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Icon(Icons.Default.Close, contentDescription = "Удалить напоминание", tint = F7Colors.TextSecondary, modifier = Modifier.size(16.dp))</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Icon(Icons.Default.Close, contentDescription = "Удалить", tint = F7Colors.TextSecondary, modifier = Modifier.size(16.dp))</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Icon(Icons.Default.InsertDriveFile, contentDescription = null, tint = EVENT_PLACEHOLDER, modifier = Modifier.size(16.dp))</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Icon(Icons.Default.KeyboardArrowDown, contentDescription = null, tint = EVENT_PLACEHOLDER, modifier = Modifier.size(16.dp))</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Icon(Icons.Default.Link, contentDescription = null, tint = F7Colors.Primary, modifier = Modifier.size(18.dp))</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Icon(Icons.Default.Notifications, contentDescription = null, tint = EVENT_PLACEHOLDER, modifier = Modifier.size(16.dp))</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$IconButton</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Row</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Text("${s.name} · ${s.email}", modifier = Modifier.clickable { onAddAttendee(s) }.padding(start = 8.dp), color = F7Colors.Primary)</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Text("Комната Talk"); Switch(checked = draft.addTalkRoom, onCheckedChange = { onDraftChange(draft.copy(addTalkRoom = it)) })</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Text("Выберите беседу", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = F7Colors.TextPrimary)</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Text("Выберите файл", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = F7Colors.TextPrimary)</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Text("Занятость участников", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = F7Colors.TextPrimary)</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Text("Настройки календаря", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = F7Colors.TextPrimary)</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Text("Повторять событие", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = F7Colors.TextPrimary)</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Text("Удалить календарь и все его события?", color = F7Colors.TextPrimary, fontSize = 14.sp, fontWeight = FontWeight.SemiBold)</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Text(att.fileName.ifBlank { att.url.substringAfterLast('/') }, color = F7Colors.TextPrimary, fontSize = 13.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f))</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Text(calendarName, color = EVENT_TEXT, fontSize = 14.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f))</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Text(entry.name, color = F7Colors.TextPrimary, fontSize = 15.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Text(label, color = EVENT_TEXT, fontSize = 14.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f))</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Text(month.month.getDisplayName(java.time.format.TextStyle.FULL_STANDALONE, java.util.Locale.forLanguageTag("ru")), style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.SemiBold)</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Text(s.address.ifBlank { s.name }, modifier = Modifier.clickable { onApplyLocation(s) }.padding(start = 8.dp), color = F7Colors.Primary)</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$confirmButton = { F7PrimaryButton(text = if (state.saving) "Сохранение…" else "Сохранить", onClick = onSave, enabled = !state.saving) }</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$if (isEdit) TextButton(onClick = onDelete, enabled = !state.deleting) { Text(if (state.deleting) "Удаление…" else "Удалить", color = F7Colors.Error) }</ID>
|
||||
<ID>MaxLineLength:CalendarViewModel.kt$CalendarViewModel$"${start.format(DateTimeFormatter.ofPattern("MMM d", Locale.ENGLISH))} – ${end.format(DateTimeFormatter.ofPattern("MMM d, yyyy", Locale.ENGLISH))}"</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$Text(room.displayName, color = F7Colors.TextPrimary, fontSize = 15.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$state.filesPickerEntries.isEmpty() -> Text("Папка пуста", color = F7Colors.TextSecondary, fontSize = 14.sp, modifier = Modifier.padding(vertical = 12.dp))</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$state.filesPickerLoading -> Text("Загрузка…", color = F7Colors.TextSecondary, fontSize = 14.sp, modifier = Modifier.padding(vertical = 12.dp))</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$state.talkPickerLoading -> Text("Загрузка…", color = F7Colors.TextSecondary, fontSize = 14.sp, modifier = Modifier.padding(vertical = 12.dp))</ID>
|
||||
<ID>MaxLineLength:CalendarExtendedUi.kt$state.talkRooms.isEmpty() -> Text("Нет доступных бесед", color = F7Colors.TextSecondary, fontSize = 14.sp, modifier = Modifier.padding(vertical = 12.dp))</ID>
|
||||
<ID>MaxLineLength:CalendarViewModel.kt$CalendarViewModel$.</ID>
|
||||
<ID>MaxLineLength:CalendarViewModel.kt$CalendarViewModel$CalendarViewMode.DAY -> st.copy(selectedDay = st.selectedDay.minusDays(1), visibleMonth = YearMonth.from(st.selectedDay.minusDays(1)))</ID>
|
||||
<ID>MaxLineLength:CalendarViewModel.kt$CalendarViewModel$CalendarViewMode.DAY -> st.copy(selectedDay = st.selectedDay.plusDays(1), visibleMonth = YearMonth.from(st.selectedDay.plusDays(1)))</ID>
|
||||
@@ -142,17 +205,27 @@
|
||||
<ID>MaxLineLength:CalendarViewModel.kt$CalendarViewModel$CalendarViewMode.WEEK -> st.copy(selectedDay = st.selectedDay.plusWeeks(1), visibleMonth = YearMonth.from(st.selectedDay.plusWeeks(1)))</ID>
|
||||
<ID>MaxLineLength:CalendarViewModel.kt$CalendarViewModel$CalendarViewMode.YEAR -> repository.loadRange(session, LocalDate.of(st.visibleYear, 1, 1), LocalDate.of(st.visibleYear, 12, 31), visibleCalendarHrefs)</ID>
|
||||
<ID>MaxLineLength:CalendarViewModel.kt$CalendarViewModel$_state.value = _state.value.copy(deleting = false, createDialogOpen = false, eventDetail = null, focusedEventUid = null, snackMessage = "Удалено")</ID>
|
||||
<ID>MaxLineLength:CalendarViewModel.kt$CalendarViewModel$_state.value = _state.value.copy(editCalendarBusy = false, editCalendarOpen = false, editCalendarTarget = null, snackMessage = "Календарь обновлён")</ID>
|
||||
<ID>MaxLineLength:CalendarViewModel.kt$CalendarViewModel$_state.value = _state.value.copy(editCalendarBusy = false, editCalendarOpen = false, editCalendarTarget = null, snackMessage = "Календарь удалён")</ID>
|
||||
<ID>MaxLineLength:CalendarViewModel.kt$CalendarViewModel$_state.value = _state.value.copy(selectedDay = day, visibleMonth = YearMonth.from(day), eventDetail = event, focusedEventUid = event.uid)</ID>
|
||||
<ID>MaxLineLength:CalendarViewModel.kt$CalendarViewModel$_state.value = _state.value.copy(selectedDay = day, visibleMonth = YearMonth.from(day), visibleYear = day.year, datePickerOpen = false)</ID>
|
||||
<ID>MaxLineLength:CalendarViewModel.kt$CalendarViewModel$classification = EventClassification.entries.firstOrNull { it.icsValue == event.classification } ?: EventClassification.PUBLIC</ID>
|
||||
<ID>MaxLineLength:CalendarViewModel.kt$CalendarViewModel$private fun formatLastSync(epoch: Long)</ID>
|
||||
<ID>MaxLineLength:CalendarViewModel.kt$CalendarViewModel$return if (visible.isEmpty()) _state.value.events else _state.value.events.filter { it.calendarHref in visible || it.calendarHref.isBlank() }</ID>
|
||||
<ID>MaxLineLength:CalendarViewModel.kt$CalendarViewModel$snackMessage = if (showSnack) "Синхронизация завершена (${result.pushedToDevice + result.pushedToServer + result.updated + result.linked})" else _state.value.snackMessage</ID>
|
||||
<ID>MaxLineLength:CalendarViewModel.kt$CalendarViewModel$updateEventDraft { it.copy(talkRoomUrl = room.callUrl, addTalkRoom = false, location = it.location.ifBlank { room.callUrl }) }</ID>
|
||||
<ID>MaximumLineLength:CalendarApiClient.kt$CalendarApiClient$ </ID>
|
||||
<ID>MaximumLineLength:CalendarComponents.kt$ </ID>
|
||||
<ID>MaximumLineLength:CalendarExtendedUi.kt$ </ID>
|
||||
<ID>MaximumLineLength:CalendarRepository.kt$CalendarRepository$ </ID>
|
||||
<ID>MaximumLineLength:CalendarViewModel.kt$CalendarViewModel$ </ID>
|
||||
<ID>MaximumLineLength:RecurrenceRule.kt$RecurrenceRule.Companion$ </ID>
|
||||
<ID>MultiLineIfElse:CalendarExtendedUi.kt$draft.date.atStartOfDay(zone).toInstant().toEpochMilli()</ID>
|
||||
<ID>MultiLineIfElse:CalendarExtendedUi.kt$draft.date.atTime( draft.startTime.substringBefore(':').toIntOrNull() ?: 10, draft.startTime.substringAfter(':').toIntOrNull() ?: 0, ).atZone(zone).toInstant().toEpochMilli()</ID>
|
||||
<ID>MultiLineIfElse:CalendarExtendedUi.kt$endDay.atTime( draft.endTime.substringBefore(':').toIntOrNull() ?: 11, draft.endTime.substringAfter(':').toIntOrNull() ?: 0, ).atZone(zone).toInstant().toEpochMilli()</ID>
|
||||
<ID>MultiLineIfElse:CalendarExtendedUi.kt$endDay.plusDays(1).atStartOfDay(zone).toInstant().toEpochMilli()</ID>
|
||||
<ID>MultiLineIfElse:RecurrenceRule.kt$RecurrenceRule.Companion$""</ID>
|
||||
<ID>NestedBlockDepth:CalendarApiClient.kt$CalendarApiClient$fun searchSharees(session: AuthSession, query: String): List<CalendarShareeSuggestion></ID>
|
||||
<ID>NoMultipleSpaces:CalendarSyncEngine.kt$CalendarSyncEngine.Companion$ </ID>
|
||||
<ID>NoUnusedImports:CalendarApiClient.kt$ru.forbion.f7cloud.feature.calendar.CalendarApiClient.kt</ID>
|
||||
<ID>NoUnusedImports:CalendarComponents.kt$ru.forbion.f7cloud.feature.calendar.CalendarComponents.kt</ID>
|
||||
@@ -162,34 +235,41 @@
|
||||
<ID>NoUnusedImports:CalendarSyncEngine.kt$ru.forbion.f7cloud.feature.calendar.CalendarSyncEngine.kt</ID>
|
||||
<ID>NoUnusedImports:DeviceCalendarClient.kt$ru.forbion.f7cloud.feature.calendar.DeviceCalendarClient.kt</ID>
|
||||
<ID>ParameterListWrapping:CalendarViewModel.kt$CalendarViewModel$(draft: CalendarCreateBookDraft)</ID>
|
||||
<ID>StringTemplate:CalendarExtendedUi.kt$${path}</ID>
|
||||
<ID>UnusedPrivateMember:CalendarComponents.kt$@Composable private fun CalendarIconButton( iconUrl: String, contentDescription: String?, onClick: () -> Unit, )</ID>
|
||||
<ID>UnusedPrivateMember:CalendarComponents.kt$@Composable private fun CalendarNavButton(text: String, onClick: () -> Unit)</ID>
|
||||
<ID>UnusedPrivateProperty:CalendarComponents.kt$private val toolbarDateFormatter = DateTimeFormatter.ofPattern("MMM d, yyyy", Locale.ENGLISH)</ID>
|
||||
<ID>UnusedPrivateProperty:CalendarSyncEngine.kt$CalendarSyncEngine$val mappedDevIds2 = mappings.map { it.deviceEventId }.toSet()</ID>
|
||||
<ID>UseCheckOrError:CalendarRepository.kt$CalendarRepository$throw IllegalStateException("Календари не найдены")</ID>
|
||||
<ID>UseCheckOrError:CalendarRepository.kt$CalendarRepository$throw IllegalStateException("Не удалось загрузить события календаря: $lastError")</ID>
|
||||
<ID>Wrapping:CalendarComponents.kt$;</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$;</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$F7PrimaryButton(text = if (saving) "Сохранение…" else "Создать", onClick = onSave, enabled = !saving)</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$F7PrimaryButton(text = if (state.saving) "Сохранение…" else "Сохранить", onClick = onSave, enabled = !state.saving)</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$Text(if (state.deleting) "Удаление…" else "Удалить", color = F7Colors.Error)</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$onAddAttendee(s)</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$onApplyLocation(s)</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$onDraftChange(draft.copy(addTalkRoom = it))</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$onDraftChange(draft.copy(calendarHref = cal.href))</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$onDraftChange(draft.copy(categories = it))</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$onDraftChange(draft.copy(classification = cl))</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$onDraftChange(draft.copy(description = it))</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$Text("До даты")</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$Text("Никогда")</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$Text("После N повторений")</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$att.url.substringAfterLast('/')</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$name = it</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$onDraftChange(draft.copy(allDay = !draft.allDay))</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$onDraftChange(draft.copy(endTime = it))</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$onDraftChange(draft.copy(mode = mode))</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$onDraftChange(draft.copy(recurrence = preset, customRrule = ""))</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$onDraftChange(draft.copy(reminderMinutes = it))</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$onDraftChange(draft.copy(reminders = draft.reminders - min))</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$onDraftChange(draft.copy(startTime = it))</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$onDraftChange(draft.copy(subscriptionUrl = it))</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$s.name</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$pickTime(draft.endTime) { onDraftChange(draft.copy(endTime = it)) }</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$pickTime(draft.startTime) { onDraftChange(draft.copy(startTime = it)) }</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$rule = rule.copy(endType = RecurrenceRule.EndType.COUNT); endMenu = false</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$rule = rule.copy(endType = RecurrenceRule.EndType.NEVER); endMenu = false</ID>
|
||||
<ID>Wrapping:CalendarExtendedUi.kt$rule = rule.copy(endType = RecurrenceRule.EndType.UNTIL, untilDate = rule.untilDate ?: LocalDate.now().plusMonths(1)); endMenu = false</ID>
|
||||
<ID>Wrapping:CalendarRepository.kt$CalendarRepository$;</ID>
|
||||
<ID>Wrapping:CalendarViewModel.kt$CalendarViewModel$;</ID>
|
||||
<ID>Wrapping:CalendarViewModel.kt$CalendarViewModel$_state.value = _state.value.copy(attachmentUploading = false, snackMessage = "Ошибка загрузки: ${it.message}")</ID>
|
||||
<ID>Wrapping:CalendarViewModel.kt$CalendarViewModel$_state.value = _state.value.copy(editCalendarBusy = false, snackMessage = "Не удалось обновить: ${it.message}")</ID>
|
||||
<ID>Wrapping:CalendarViewModel.kt$CalendarViewModel$_state.value = _state.value.copy(editCalendarBusy = false, snackMessage = "Не удалось удалить: ${it.message}")</ID>
|
||||
<ID>Wrapping:CalendarViewModel.kt$CalendarViewModel$_state.value = _state.value.copy(loading = false, error = it.message, unauthorized = it is UnauthorizedException)</ID>
|
||||
<ID>Wrapping:CalendarViewModel.kt$CalendarViewModel$it.calendarHref in visible || it.calendarHref.isBlank()</ID>
|
||||
<ID>Wrapping:CalendarViewModel.kt$CalendarViewModel$it.copy(talkRoomUrl = room.callUrl, addTalkRoom = false, location = it.location.ifBlank { room.callUrl })</ID>
|
||||
<ID>Wrapping:CalendarViewModel.kt$CalendarViewModel$it.icsValue == event.classification</ID>
|
||||
<ID>Wrapping:CalendarViewModel.kt$CalendarViewModel$room.callUrl</ID>
|
||||
<ID>Wrapping:CalendarViewModel.kt$CalendarViewModel${ _state.value = _state.value.copy(createBookOpen = false, createBookDraft = CalendarCreateBookDraft()) }</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
package ru.forbion.f7cloud.feature.calendar
|
||||
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
import java.time.temporal.ChronoField
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
/**
|
||||
* Локальный разворот повторяющихся событий (P1, mail/072/093).
|
||||
*
|
||||
* Генерирует даты-старты оккуренций правила RRULE в заданном диапазоне БЕЗ обращения к серверу
|
||||
* (`<c:expand>` не используем — это правка core, mail/072). Поддерживается то же подмножество,
|
||||
* что умеет создавать нативный редактор ([RecurrenceRule]): FREQ=DAILY|WEEKLY|MONTHLY|YEARLY,
|
||||
* INTERVAL, BYDAY (для WEEKLY), окончание UNTIL/COUNT/никогда.
|
||||
*
|
||||
* Развёртка делается на КАЖДЫЙ показ (не кэшируется — иначе окно диапазона «вмораживается»,
|
||||
* mail/072). Экземпляры различимы парой (uid, дата-старт оккуренции).
|
||||
*/
|
||||
object CalendarRecurrence {
|
||||
|
||||
/** Жёсткий предел числа шагов генератора — страховка от бесконечных/патологических правил. */
|
||||
private const val MAX_STEPS = 3660
|
||||
|
||||
/**
|
||||
* Покрывает ли событие [day] с учётом повторения.
|
||||
*
|
||||
* @param start дата-старт мастер-события (в нужной зоне).
|
||||
* @param spanDays длительность события в днях для all-day (>=1); для timed игнорируется.
|
||||
* @param allDay all-day событие занимает [occStart, occStart+spanDays); timed — только occStart.
|
||||
*/
|
||||
fun occursOnDay(
|
||||
rule: RecurrenceRule,
|
||||
start: LocalDate,
|
||||
day: LocalDate,
|
||||
spanDays: Long,
|
||||
allDay: Boolean,
|
||||
): Boolean {
|
||||
if (day.isBefore(start)) return false
|
||||
val span = if (allDay) spanDays.coerceAtLeast(1) else 1L
|
||||
// Оккуренция покрывает day, если её старт в диапазоне (day - (span-1) .. day).
|
||||
val earliestStart = day.minusDays(span - 1)
|
||||
var found = false
|
||||
forEachOccurrenceStart(rule, start, upTo = day) { occStart ->
|
||||
if (!occStart.isBefore(earliestStart) && !occStart.isAfter(day)) {
|
||||
found = true
|
||||
false // стоп
|
||||
} else {
|
||||
true // продолжаем
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
/** Список дат-стартов оккуренций в [rangeStart, rangeEnd] включительно (для списковых видов). */
|
||||
fun occurrenceStartsInRange(
|
||||
rule: RecurrenceRule,
|
||||
start: LocalDate,
|
||||
rangeStart: LocalDate,
|
||||
rangeEnd: LocalDate,
|
||||
): List<LocalDate> {
|
||||
val out = mutableListOf<LocalDate>()
|
||||
forEachOccurrenceStart(rule, start, upTo = rangeEnd) { occStart ->
|
||||
if (!occStart.isBefore(rangeStart)) out.add(occStart)
|
||||
true
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** Границы окончания правила: (UNTIL-дата или null, максимум оккуренций по COUNT). */
|
||||
private fun bounds(rule: RecurrenceRule): Pair<LocalDate?, Int> {
|
||||
val until = if (rule.endType == RecurrenceRule.EndType.UNTIL) rule.untilDate else null
|
||||
val maxCount = if (rule.endType == RecurrenceRule.EndType.COUNT) rule.count.coerceAtLeast(1) else Int.MAX_VALUE
|
||||
return until to maxCount
|
||||
}
|
||||
|
||||
/**
|
||||
* Перебирает даты-старты оккуренций от [start], пока не превышена [upTo], не исчерпан
|
||||
* UNTIL/COUNT или лимит шагов. [onOccurrence] возвращает false, чтобы прервать перебор.
|
||||
*/
|
||||
private inline fun forEachOccurrenceStart(
|
||||
rule: RecurrenceRule,
|
||||
start: LocalDate,
|
||||
upTo: LocalDate,
|
||||
onOccurrence: (LocalDate) -> Boolean,
|
||||
) {
|
||||
if (rule.freq == "WEEKLY" && rule.byDays.isNotEmpty()) {
|
||||
forEachWeeklyByDay(rule, start, upTo, onOccurrence)
|
||||
} else {
|
||||
forEachSimpleStep(rule, start, upTo, onOccurrence)
|
||||
}
|
||||
}
|
||||
|
||||
/** WEEKLY с BYDAY: раскрываем выбранные дни недели в каждом интервал-блоке недель. */
|
||||
private inline fun forEachWeeklyByDay(
|
||||
rule: RecurrenceRule,
|
||||
start: LocalDate,
|
||||
upTo: LocalDate,
|
||||
onOccurrence: (LocalDate) -> Boolean,
|
||||
) {
|
||||
val interval = rule.interval.coerceAtLeast(1)
|
||||
val (until, maxCount) = bounds(rule)
|
||||
val weeklyDows = RecurrenceRule.ICS_DAYS.filter { it in rule.byDays }.map { icsDayToDow(it).value.toLong() }
|
||||
val weekBase = mondayOf(start)
|
||||
var emitted = 0
|
||||
var block = 0L
|
||||
while (block < MAX_STEPS) {
|
||||
val weekStart = weekBase.plusWeeks(block * interval)
|
||||
if (weekStart.isAfter(upTo)) return // все дни этой и следующих недель уже за upTo
|
||||
for (dowValue in weeklyDows) {
|
||||
val occ = weekStart.with(ChronoField.DAY_OF_WEEK, dowValue)
|
||||
if (occ.isBefore(start) || occ.isAfter(upTo)) continue
|
||||
if (until != null && occ.isAfter(until)) return
|
||||
if (emitted >= maxCount) return
|
||||
emitted++
|
||||
if (!onOccurrence(occ)) return
|
||||
}
|
||||
block++
|
||||
}
|
||||
}
|
||||
|
||||
/** DAILY / WEEKLY(без BYDAY) / MONTHLY / YEARLY: простой шаг по мастер-старту. */
|
||||
private inline fun forEachSimpleStep(
|
||||
rule: RecurrenceRule,
|
||||
start: LocalDate,
|
||||
upTo: LocalDate,
|
||||
onOccurrence: (LocalDate) -> Boolean,
|
||||
) {
|
||||
val interval = rule.interval.coerceAtLeast(1)
|
||||
val (until, maxCount) = bounds(rule)
|
||||
var emitted = 0
|
||||
var n = 0L
|
||||
while (n < MAX_STEPS) {
|
||||
val occ = when (rule.freq) {
|
||||
"DAILY" -> start.plusDays(n * interval)
|
||||
"WEEKLY" -> start.plusWeeks(n * interval)
|
||||
"MONTHLY" -> start.plusMonths(n * interval)
|
||||
"YEARLY" -> start.plusYears(n * interval)
|
||||
else -> return
|
||||
}
|
||||
val pastUntil = until != null && occ.isAfter(until)
|
||||
if (occ.isAfter(upTo) || pastUntil || emitted >= maxCount) return
|
||||
emitted++
|
||||
if (!onOccurrence(occ)) return
|
||||
n++
|
||||
}
|
||||
}
|
||||
|
||||
private fun mondayOf(date: LocalDate): LocalDate = date.with(ChronoField.DAY_OF_WEEK, 1L)
|
||||
|
||||
private fun icsDayToDow(ics: String): DayOfWeek = when (ics) {
|
||||
"MO" -> DayOfWeek.MONDAY
|
||||
"TU" -> DayOfWeek.TUESDAY
|
||||
"WE" -> DayOfWeek.WEDNESDAY
|
||||
"TH" -> DayOfWeek.THURSDAY
|
||||
"FR" -> DayOfWeek.FRIDAY
|
||||
"SA" -> DayOfWeek.SATURDAY
|
||||
else -> DayOfWeek.SUNDAY
|
||||
}
|
||||
|
||||
/** Число дней-длительности для all-day по границам [start,end) (в датах). */
|
||||
fun allDaySpanDays(start: LocalDate, end: LocalDate): Long =
|
||||
ChronoUnit.DAYS.between(start, end).coerceAtLeast(1)
|
||||
}
|
||||
+7
@@ -430,6 +430,13 @@ class CalendarRepository(
|
||||
fun eventOccursOnDay(event: CalendarEventItem, day: LocalDate, zone: ZoneId = ZoneId.systemDefault()): Boolean {
|
||||
val start = Instant.ofEpochMilli(event.startEpochMilli).atZone(zone).toLocalDate()
|
||||
val end = Instant.ofEpochMilli(event.endEpochMilli).atZone(zone).toLocalDate()
|
||||
// Повторяющееся событие: разворачиваем оккуренции на лету (P1, mail/072/093).
|
||||
val rule = RecurrenceRule.parse(event.rrule)
|
||||
if (rule != null) {
|
||||
val spanDays = if (event.allDay) CalendarRecurrence.allDaySpanDays(start, end) else 1L
|
||||
return CalendarRecurrence.occursOnDay(rule, start, day, spanDays, event.allDay)
|
||||
}
|
||||
// Одиночное событие: прежнее поведение (all-day занимает [start, end)).
|
||||
return if (event.allDay) !day.isBefore(start) && day.isBefore(end) else day == start
|
||||
}
|
||||
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package ru.forbion.f7cloud.feature.calendar
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.time.LocalDate
|
||||
|
||||
/**
|
||||
* Разворот повторений (P1): [CalendarRecurrence.occursOnDay] / [occurrenceStartsInRange].
|
||||
*/
|
||||
class CalendarRecurrenceTest {
|
||||
|
||||
private fun rule(s: String) = RecurrenceRule.parse(s)!!
|
||||
private fun d(s: String) = LocalDate.parse(s)
|
||||
|
||||
private fun occurs(rrule: String, start: String, day: String, span: Long = 1, allDay: Boolean = false) =
|
||||
CalendarRecurrence.occursOnDay(rule(rrule), d(start), d(day), span, allDay)
|
||||
|
||||
@Test fun daily_hits_every_day() {
|
||||
assertTrue(occurs("FREQ=DAILY", "2026-07-01", "2026-07-01"))
|
||||
assertTrue(occurs("FREQ=DAILY", "2026-07-01", "2026-07-15"))
|
||||
assertFalse("до старта — нет", occurs("FREQ=DAILY", "2026-07-10", "2026-07-05"))
|
||||
}
|
||||
|
||||
@Test fun daily_interval_2_skips_odd_offsets() {
|
||||
assertTrue(occurs("FREQ=DAILY;INTERVAL=2", "2026-07-01", "2026-07-03"))
|
||||
assertFalse(occurs("FREQ=DAILY;INTERVAL=2", "2026-07-01", "2026-07-02"))
|
||||
}
|
||||
|
||||
@Test fun weekly_same_weekday() {
|
||||
// 2026-07-01 — среда; +неделя = 08, 15…
|
||||
assertTrue(occurs("FREQ=WEEKLY", "2026-07-01", "2026-07-08"))
|
||||
assertTrue(occurs("FREQ=WEEKLY", "2026-07-01", "2026-07-29"))
|
||||
assertFalse(occurs("FREQ=WEEKLY", "2026-07-01", "2026-07-09"))
|
||||
}
|
||||
|
||||
@Test fun weekly_byday_mon_wed_fri() {
|
||||
// старт среда 2026-07-01; BYDAY=MO,WE,FR
|
||||
val r = "FREQ=WEEKLY;BYDAY=MO,WE,FR"
|
||||
assertTrue("среда старта", occurs(r, "2026-07-01", "2026-07-01"))
|
||||
assertTrue("пятница той же недели", occurs(r, "2026-07-01", "2026-07-03"))
|
||||
assertTrue("понедельник след. недели", occurs(r, "2026-07-01", "2026-07-06"))
|
||||
assertFalse("вторник — нет", occurs(r, "2026-07-01", "2026-07-07"))
|
||||
assertFalse("понедельник ДО старта той же недели — нет", occurs(r, "2026-07-01", "2026-06-29"))
|
||||
}
|
||||
|
||||
@Test fun weekly_byday_interval_2_skips_middle_week() {
|
||||
val r = "FREQ=WEEKLY;INTERVAL=2;BYDAY=MO"
|
||||
// старт среда 01.07; понедельники: блок0 нед. 29.06 (до старта, нет), блок2 нед. 13.07 → 13
|
||||
assertTrue(occurs(r, "2026-07-01", "2026-07-13"))
|
||||
assertFalse("промежуточная неделя 06.07 пропущена", occurs(r, "2026-07-01", "2026-07-06"))
|
||||
}
|
||||
|
||||
@Test fun monthly_same_day_of_month() {
|
||||
assertTrue(occurs("FREQ=MONTHLY", "2026-01-15", "2026-04-15"))
|
||||
assertFalse(occurs("FREQ=MONTHLY", "2026-01-15", "2026-04-16"))
|
||||
}
|
||||
|
||||
@Test fun yearly() {
|
||||
assertTrue(occurs("FREQ=YEARLY", "2024-02-29", "2028-02-29"))
|
||||
assertTrue(occurs("FREQ=YEARLY", "2026-03-10", "2030-03-10"))
|
||||
assertFalse(occurs("FREQ=YEARLY", "2026-03-10", "2027-03-11"))
|
||||
}
|
||||
|
||||
@Test fun count_limits_occurrences() {
|
||||
// COUNT=3 → 01, 02, 03; 04 уже нет
|
||||
assertTrue(occurs("FREQ=DAILY;COUNT=3", "2026-07-01", "2026-07-03"))
|
||||
assertFalse(occurs("FREQ=DAILY;COUNT=3", "2026-07-01", "2026-07-04"))
|
||||
}
|
||||
|
||||
@Test fun until_bounds_occurrences() {
|
||||
assertTrue(occurs("FREQ=DAILY;UNTIL=20260705T235959Z", "2026-07-01", "2026-07-05"))
|
||||
assertFalse(occurs("FREQ=DAILY;UNTIL=20260705T235959Z", "2026-07-01", "2026-07-06"))
|
||||
}
|
||||
|
||||
@Test fun allday_multiday_occurrence_covers_span() {
|
||||
// 2-дневное all-day (span=2), еженедельно: оккуренция 08.07 покрывает 08 и 09
|
||||
assertTrue(occurs("FREQ=WEEKLY", "2026-07-01", "2026-07-09", span = 2, allDay = true))
|
||||
assertFalse(occurs("FREQ=WEEKLY", "2026-07-01", "2026-07-10", span = 2, allDay = true))
|
||||
}
|
||||
|
||||
@Test fun range_listing() {
|
||||
val starts = CalendarRecurrence.occurrenceStartsInRange(
|
||||
rule = rule("FREQ=WEEKLY"),
|
||||
start = d("2026-07-01"),
|
||||
rangeStart = d("2026-07-05"),
|
||||
rangeEnd = d("2026-07-25"),
|
||||
)
|
||||
assertEquals(listOf(d("2026-07-08"), d("2026-07-15"), d("2026-07-22")), starts)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user