Compare commits
28 Commits
483e4107d7
...
fff360fe39
| Author | SHA1 | Date | |
|---|---|---|---|
| fff360fe39 | |||
| d047dea210 | |||
| cfa9fe2ffd | |||
| e25dcbc403 | |||
| 342e1e497d | |||
| 6f82a3fc56 | |||
| fe330be510 | |||
| 58d56feb76 | |||
| 58eed74b03 | |||
| 8186031676 | |||
| 9e5a16a605 | |||
| b56c9a5998 | |||
| d50eaaed3d | |||
| 3824ad86f2 | |||
| 89e32d2875 | |||
| a30af98a8d | |||
| fda34a5b69 | |||
| 3d9f3819d9 | |||
| 04f67e071b | |||
| b83d3dc872 | |||
| 530663754a | |||
| 8cee4d61bf | |||
| 2220fe4a89 | |||
| a727e2dbd1 | |||
| 335468b80f | |||
| b15a97d30e | |||
| 79e6b4ec83 | |||
| 8c3d422c85 |
@@ -2,6 +2,40 @@
|
|||||||
|
|
||||||
Формат: `ГГГГ-ММ-ДД | версия | изменение | контракты | риск`
|
Формат: `ГГГГ-ММ-ДД | версия | изменение | контракты | риск`
|
||||||
|
|
||||||
|
- 2026-07-09 | v0.5.123 (131) | Первый тегированный релиз в Gitea (тег `v0.5.123` + Release с APK и архивом исходников GPL). Включает хвосты этапов 0/1: FLAG_SECURE на экранах входа/PIN (аудит п.3, коммит cfa9fe2) + активированный CI-раннер на инфре B (docs/CI). Функциональных изменений в приложении относительно v0.5.122 нет, кроме FLAG_SECURE | контракты не менялись | низкий
|
||||||
|
|
||||||
|
- 2026-07-08 | v0.5.122 (130) | Убрана кнопка «чаты» из нижней панели (слева теперь всегда «назад», единый layout во всех разделах, включая Конференции) + убрана кнопка обновления ↻ сверху справа в Конференциях (onRefresh у F7ModuleScreen; список грузится при открытии/по push) | контракты не менялись | низкий: только UI
|
||||||
|
|
||||||
|
- 2026-07-08 | v0.5.121 (129) | Нижняя панель: убрана зелёная подложка под активной кнопкой (фон всегда нейтральный). Активный раздел больше не выделяется фоном (зелёных вариантов иконок разделов нет). Зелёные иконки чатов/бургера при открытии остаются | контракты не менялись | низкий: только фон кнопок
|
||||||
|
|
||||||
|
- 2026-07-08 | v0.5.120 (128) | Нижняя панель: иконки-разделы (Почта/Карточки/Конференции) крупнее — 46dp (у серверных SVG внутренний «воздух», глиф оставался мелким). Стрелка/бургер — прежние 34dp | контракты не менялись | низкий: только размер иконок
|
||||||
|
|
||||||
|
- 2026-07-08 | v0.5.119 (127) | Убран чекбокс «Доверять сертификату» с экрана входа (раньше только в debug, теперь совсем). trustAllCerts=false по умолчанию; trust-all на сети/WebView в release и так отключён | контракты не менялись | низкий: только UI входа
|
||||||
|
|
||||||
|
- 2026-07-08 | v0.5.118 (126) | Первая RELEASE-сборка на боевом ключе владельца (release-keystore.jks, alias f7cloud, SHA-256 5F:E5:F0:6C…). Раньше отдавались debug-сборки (debug-ключ сервера) → Play Защита ругалась «незнакомый разработчик». Теперь release, подписан ключом владельца → предупреждения нет, обновления ставятся поверх. signing.env читает креды из /root/.f7cloud-keys/key.properties (в git НЕ коммитится). + иконки нижней панели крупнее (24dp→34dp, заполняют кружок) | контракты не менялись | средний: смена контура подписи на боевой ключ; debug-сборку v117 надо удалить перед установкой (иная подпись)
|
||||||
|
|
||||||
|
- 2026-07-08 | v0.5.117 (125) | Нижняя панель → навигационная по дизайну «Новое меню» (other/Новое меню.png): слева назад (в Конференциях — чаты), ярлыки-разделы Почта · Карточки · Конференции (активный подсвечен), справа выдвижное меню. Профиль/Уведомления/Настройки/Создать убраны из панели (первые три — в выдвижном меню, бейдж уведомлений на кнопке меню). `F7BottomBarConfig` упрощён, `F7MobileBottomBar` +слоты SectionMail/Cards/Conferences +activeTabKey. Создание Файлов/Контактов вызывалось ТОЛЬКО из панели → новый `F7CreateButton` (зелёный «+») в шапках Файлов и Контактов (Задачи/Поддержка/Почта свои имели) | контракты не менялись | средний: изменена навигация нижней панели; проверить переходы разделов и создание в Файлах/Контактах
|
||||||
|
|
||||||
|
- 2026-07-08 | v0.5.117 (125) | Дизайн выдвижного меню: сетка 3 колонки (было 4), ячейка кликабельна целиком, подпись во всю ширину. Состав пунктов точно по дизайну — Файлы/Календарь/Контакты/Карточки/Заметки + Bitrix/1C/Личный кабинет + Уведомления/Настройки (убраны Почта/Конференции/Задачи/Поддержка — они в нижней панели). Уведомления→шит, Настройки→настройки активного таба (нативные пункты рисуются локальной иконкой в круглом бейдже) | контракты не менялись | низкий: состав/вид меню
|
||||||
|
|
||||||
|
- 2026-07-08 | v0.5.117 (125) | Нативная панель профиля вместо веб-ЛК forbion: «Личный кабинет» открывал внешнюю ссылку (десктопный дропдаун forbion, криво на моб.) → теперь нативный bottom-card (ProfileSheet): аватар-инициал + имя (OCS cloud/user) + Открыть профиль/QR + Установить статус/Личные настройки/Учётные записи (deep-link веб) + О программе (нативный About) + Выйти. Точки входа: пункт меню + кнопка профиля | контракты не менялись; +GET ocs/cloud/user (имя) | низкий: только панель профиля
|
||||||
|
|
||||||
|
- 2026-07-08 | v0.5.117 (125) | Fix insets (edge-to-edge, targetSdk 36): контент залезал под системную навигацию снизу — добавлен нижний inset (`f7SafeBottomInsets`) к контенту в F7AppScaffold (верх уже был). | контракты не менялись | низкий: отступы контента
|
||||||
|
|
||||||
|
- 2026-07-08 | v0.5.117 (125) | Fix отображения писем: HTML-письма (широкие таблицы Битрикс/рассылок) уезжали за правый край — WebView без useWideViewPort игнорировал viewport width=device-width. Включены useWideViewPort+loadWithOverviewMode+пинч-зум | контракты не менялись | низкий: только рендер письма
|
||||||
|
|
||||||
|
- 2026-07-08 | v0.5.116 (124) | Меню-лончер (сетка приложений) → bottom-sheet на пол-экрана (требование владельца «выходить снизу вверх только на пол экрана»). `F7AppMenuSheet` был `fillMaxSize` с непрозрачным фоном → закрывал весь экран. Теперь: якорь снизу (`Alignment.BottomCenter`), высота `fillMaxHeight(0.6f)`, скруглённый верх (20dp) + тень, ручка-хендл, лёгкий скрим (0.18) над листом с тапом-для-закрытия, зазор `BottomBarReserve` под нижнюю панель, сетка иконок скроллится (`weight(1f)`). Экран под меню теперь виден сверху. Компилируется | контракты не менялись | низкий: только оверлей меню
|
||||||
|
|
||||||
|
- 2026-07-08 | v0.5.115 (123) | Бамп версии под дизайн-веху (0.5.114→0.5.115, code 122→123, стиль проекта: патч +1 / code +1). До этого Этапы 0/1 + весь дизайн ошибочно шли одной WIP-линией 0.5.114 (122) — теперь ревью-сборки дизайна помечены отдельной версией. Дальше — бампить на каждую заметную веху/ревью-сборку | версионирование | нет: только versionCode/Name
|
||||||
|
|
||||||
|
- 2026-07-08 | v0.5.114 (122) | Дизайн Календаря (месячная сетка): по «Календарь · Месяц» — дни недели вынесены в серые пилюли; сетка стала безрамочной с тонкими гридлайнами (0.5dp Grey3) вместо скруглённых боксов; номер дня сверху-слева; сегодня — белое число в зелёном круге; событие — зелёная пилюля PrimaryLight (до 2 шт + «+N») вместо одного превью/точки. Компактная мобильная модель (месяц сверху + список событий выбранного дня снизу) сохранена — не десктопная таблица. `CalendarMonthGrid`/`CalendarDayCell`, убран неиспользуемый параметр daysWithEvents. Проверено сборкой | контракты не менялись | низкий: только вид месяца
|
||||||
|
|
||||||
|
- 2026-07-08 | v0.5.114 (122) | Дизайн Файлов (правая панель свойств/доступа): `FilesDetailsSheet` по «Правая панель свойства/доступ» — табы из текст+подчёркивание → две иконки-пилюли (Обсуждение 💬 / Доступ 👥, активная — зелёная заливка PrimaryLight); шапка: крестик в правом верхнем углу, чип владельца (аватар-инициал + логин), дата/размер справа, звезда-избранное (wired на vm.toggleFavorite) вместо строки «Владелец…»; кнопка отправки комментария → зелёный круг-градиент с белой стрелкой. Иконки — Material (extended уже в модуле). Проверено сборкой | контракты не менялись | низкий: визуал панели, логика (шэры/события/избранное) та же
|
||||||
|
|
||||||
|
- 2026-07-08 | v0.5.114 (122) | Дизайн Конференций (подложка чата): `TalkChatBackground` грузил chat-pattern.svg с сервера поверх плоского фона → заменён на бандл-ресурс `R.drawable.chat_background` (Подложка2 из дизайна — светлый градиент + водяной паттерн иконок, владелец выгрузил). PNG 481КБ → WebP 49КБ (q82, ×10), `feature/talk/res/drawable-nodpi`. ContentScale.Crop, full-bleed. Офлайн + точно из макета. Проверено сборкой | контракты не менялись | низкий: только фон экрана чата
|
||||||
|
|
||||||
|
- 2026-07-08 | v0.5.114 (122) | Дизайн Почты (теги + сверка экрана сообщения): цвета тегов-пилюль сняты пипеткой с макета «Почта · Список сообщений» — Работа #DAEDD6/#28A413, Позже #F1EEE0/#C1B464, Личное #EFE4FB/#B574F7 (fallback tagChipColors, когда сервер не отдаёт colorHex; раньше приблизительные). Экран СООБЩЕНИЯ сверен с «Почта · Окно сообщения» — уже совпадает (тема-заголовок, sender-card, тело, плавающая пилюля «↩ Ответить» MailReplyFab на градиенте Primary), правок не требует. Проверено сборкой | контракты не менялись | низкий: только цвета тегов
|
||||||
|
|
||||||
- 2026-07-08 | v0.5.114 (122) | Дизайн-палитра из Figma «Брендбук» (владелец) в F7Colors: точные Base-токены (Black #151515, Grey1 #808080, Grey2 #F5F5F5, Grey3 #E6E6E6, Green #70B62B +30%/10%, Yellow #F6E120, Purple #9747FF, Red #FF7A66 +10%). Семантические токены (TextPrimary/Secondary/Border/SurfaceMuted/Primary) переведены на Base — основа сверена, совпала. **ИСПРАВЛЕНО расхождение: Error #D74642 → #FF7A66 (дизайн-красный)** — влияет на кнопки удаления/ошибки во ВСЁМ приложении. Yellow/Purple — под теги. Пропагируется на все экраны; шаг к закрытию аудитного долга «77 захардкоженных Color(0x…)» | дизайн-система | средний: Error-красный меняется app-wide (по дизайну); проверить экраны удаления/ошибок
|
- 2026-07-08 | v0.5.114 (122) | Дизайн-палитра из Figma «Брендбук» (владелец) в F7Colors: точные Base-токены (Black #151515, Grey1 #808080, Grey2 #F5F5F5, Grey3 #E6E6E6, Green #70B62B +30%/10%, Yellow #F6E120, Purple #9747FF, Red #FF7A66 +10%). Семантические токены (TextPrimary/Secondary/Border/SurfaceMuted/Primary) переведены на Base — основа сверена, совпала. **ИСПРАВЛЕНО расхождение: Error #D74642 → #FF7A66 (дизайн-красный)** — влияет на кнопки удаления/ошибки во ВСЁМ приложении. Yellow/Purple — под теги. Пропагируется на все экраны; шаг к закрытию аудитного долга «77 захардкоженных Color(0x…)» | дизайн-система | средний: Error-красный меняется app-wide (по дизайну); проверить экраны удаления/ошибок
|
||||||
|
|
||||||
- 2026-07-08 | v0.5.114 (122) | Дизайн Файлов (чанк 3 — каркас списка): рескин главного экрана под мобильный макет (Figma files/*.png на инфре B). Новый FilesBrowserChrome.kt: поиск-пилюля + кнопка-переключатель список⇄сетка, хлебные крошки (🏠 › путь, навигация navigateToPath), фильтр-чипы (Тип файла/Участники/Изменен) ВМЕСТО десктопной колоночной шапки, вид СЕТКА (LazyVerticalGrid 2 кол., карточки), пустое состояние (иконка+заголовок+подзаголовок). Строки без разделителей (по макету). На токенах F7Spacing/F7Radius. material-icons-extended → feature:files (иконки chrome, потом точные из дизайна). Компилируется | контракты не менялись | средний: заметный UI-рескин Файлов; логика (загрузка/выбор/открытие) та же, сверка на устройстве
|
- 2026-07-08 | v0.5.114 (122) | Дизайн Файлов (чанк 3 — каркас списка): рескин главного экрана под мобильный макет (Figma files/*.png на инфре B). Новый FilesBrowserChrome.kt: поиск-пилюля + кнопка-переключатель список⇄сетка, хлебные крошки (🏠 › путь, навигация navigateToPath), фильтр-чипы (Тип файла/Участники/Изменен) ВМЕСТО десктопной колоночной шапки, вид СЕТКА (LazyVerticalGrid 2 кол., карточки), пустое состояние (иконка+заголовок+подзаголовок). Строки без разделителей (по макету). На токенах F7Spacing/F7Radius. material-icons-extended → feature:files (иконки chrome, потом точные из дизайна). Компилируется | контракты не менялись | средний: заметный UI-рескин Файлов; логика (загрузка/выбор/открытие) та же, сверка на устройстве
|
||||||
|
|||||||
+2
-2
@@ -39,8 +39,8 @@ android {
|
|||||||
applicationId 'ru.forbion.f7cloud.mobile'
|
applicationId 'ru.forbion.f7cloud.mobile'
|
||||||
minSdk 26
|
minSdk 26
|
||||||
targetSdk 36
|
targetSdk 36
|
||||||
versionCode 122
|
versionCode 131
|
||||||
versionName '0.5.114'
|
versionName '0.5.123'
|
||||||
missingDimensionStrategy 'default', 'f7'
|
missingDimensionStrategy 'default', 'f7'
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import androidx.lifecycle.ProcessLifecycleOwner
|
|||||||
import ru.forbion.f7cloud.core.auth.AppLockStore
|
import ru.forbion.f7cloud.core.auth.AppLockStore
|
||||||
import ru.forbion.f7cloud.core.designsystem.F7Colors
|
import ru.forbion.f7cloud.core.designsystem.F7Colors
|
||||||
import ru.forbion.f7cloud.core.designsystem.F7PrimaryButton
|
import ru.forbion.f7cloud.core.designsystem.F7PrimaryButton
|
||||||
|
import ru.forbion.f7cloud.core.designsystem.F7SecureScreen
|
||||||
import ru.forbion.f7cloud.core.designsystem.F7TextButton
|
import ru.forbion.f7cloud.core.designsystem.F7TextButton
|
||||||
import ru.forbion.f7cloud.mobile.R
|
import ru.forbion.f7cloud.mobile.R
|
||||||
|
|
||||||
@@ -145,6 +146,7 @@ fun AppLockSetupDialog(
|
|||||||
onLockConfigured: () -> Unit = {},
|
onLockConfigured: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
if (!visible) return
|
if (!visible) return
|
||||||
|
F7SecureScreen() // создание/подтверждение PIN — не в скриншотах/recents
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val activity = context.findFragmentActivity()
|
val activity = context.findFragmentActivity()
|
||||||
@@ -534,6 +536,7 @@ private fun AppLockUnlockScreen(
|
|||||||
lockSession: Int,
|
lockSession: Int,
|
||||||
onUnlocked: () -> Unit,
|
onUnlocked: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
F7SecureScreen() // экран разблокировки (PIN) — не в скриншотах/recents
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val activity = context.findFragmentActivity()
|
val activity = context.findFragmentActivity()
|
||||||
var pin by remember { mutableStateOf("") }
|
var pin by remember { mutableStateOf("") }
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package ru.forbion.f7cloud.mobile.ui
|
package ru.forbion.f7cloud.mobile.ui
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Notifications
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import ru.forbion.f7cloud.core.designsystem.F7AppMenuItem
|
import ru.forbion.f7cloud.core.designsystem.F7AppMenuItem
|
||||||
|
|
||||||
enum class AppTab(
|
enum class AppTab(
|
||||||
@@ -29,16 +32,14 @@ private data class AppMenuEntry(
|
|||||||
val webPath: String? = null,
|
val webPath: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Состав меню — по дизайну выдвижного меню (Почта/Конференции/Задачи/Поддержка — только
|
||||||
|
// в нижней панели; Уведомления/Настройки добавлены как нативные пункты, см. appMenuItems).
|
||||||
private val coreAppMenuEntries = listOf(
|
private val coreAppMenuEntries = listOf(
|
||||||
AppMenuEntry(AppTab.Mail, "Почта", "mail-glass.svg"),
|
|
||||||
AppMenuEntry(AppTab.Files, "Файлы", "files-glass.svg"),
|
AppMenuEntry(AppTab.Files, "Файлы", "files-glass.svg"),
|
||||||
AppMenuEntry(AppTab.Calendar, "Календарь", "calendar-glass.svg"),
|
AppMenuEntry(AppTab.Calendar, "Календарь", "calendar-glass.svg"),
|
||||||
AppMenuEntry(AppTab.Contacts, "Контакты", "contact-glass.svg"),
|
AppMenuEntry(AppTab.Contacts, "Контакты", "contact-glass.svg"),
|
||||||
AppMenuEntry(AppTab.Talk, "Конференции", "spreed-glass.svg"),
|
|
||||||
AppMenuEntry(AppTab.Deck, "Карточки", "deck-glass.svg"),
|
AppMenuEntry(AppTab.Deck, "Карточки", "deck-glass.svg"),
|
||||||
AppMenuEntry(AppTab.Tasks, "Задачи", "task-glass.svg"),
|
|
||||||
AppMenuEntry(null, "Заметки", "notes-glass.svg", webPath = "/apps/notes/"),
|
AppMenuEntry(null, "Заметки", "notes-glass.svg", webPath = "/apps/notes/"),
|
||||||
AppMenuEntry(AppTab.Support, "Поддержка", "icon-header-f7support.svg"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data class AppMenuExternalSite(
|
data class AppMenuExternalSite(
|
||||||
@@ -71,7 +72,22 @@ fun appMenuItems(
|
|||||||
externalUrl = site.openUrl,
|
externalUrl = site.openUrl,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return core + external
|
// Нативные пункты по дизайну — действия обрабатываются в AppScaffold по label
|
||||||
|
val native = listOf(
|
||||||
|
F7AppMenuItem(
|
||||||
|
label = "Уведомления",
|
||||||
|
iconUrl = "",
|
||||||
|
selected = false,
|
||||||
|
localIcon = Icons.Filled.Notifications,
|
||||||
|
),
|
||||||
|
F7AppMenuItem(
|
||||||
|
label = "Настройки",
|
||||||
|
iconUrl = "",
|
||||||
|
selected = false,
|
||||||
|
localIcon = Icons.Filled.Settings,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return core + external + native
|
||||||
}
|
}
|
||||||
|
|
||||||
fun appTabFromMenuIndex(index: Int): AppTab? {
|
fun appTabFromMenuIndex(index: Int): AppTab? {
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.material3.Checkbox
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
@@ -82,6 +81,7 @@ import ru.forbion.f7cloud.core.push.F7PushEventHub
|
|||||||
import ru.forbion.f7cloud.core.push.F7PushRegistrar
|
import ru.forbion.f7cloud.core.push.F7PushRegistrar
|
||||||
import ru.forbion.f7cloud.core.designsystem.F7OverlayNavigationProvider
|
import ru.forbion.f7cloud.core.designsystem.F7OverlayNavigationProvider
|
||||||
import ru.forbion.f7cloud.core.designsystem.F7AppMenuSheet
|
import ru.forbion.f7cloud.core.designsystem.F7AppMenuSheet
|
||||||
|
import ru.forbion.f7cloud.core.designsystem.F7SecureScreen
|
||||||
import ru.forbion.f7cloud.core.designsystem.F7AppScaffold
|
import ru.forbion.f7cloud.core.designsystem.F7AppScaffold
|
||||||
import ru.forbion.f7cloud.core.designsystem.F7AutoHideBottomBar
|
import ru.forbion.f7cloud.core.designsystem.F7AutoHideBottomBar
|
||||||
import ru.forbion.f7cloud.core.designsystem.F7BottomBarActions
|
import ru.forbion.f7cloud.core.designsystem.F7BottomBarActions
|
||||||
@@ -483,6 +483,7 @@ fun AppScaffold(
|
|||||||
serverUrl = currentSession.serverUrl,
|
serverUrl = currentSession.serverUrl,
|
||||||
userId = userId,
|
userId = userId,
|
||||||
config = bottomBarConfig,
|
config = bottomBarConfig,
|
||||||
|
activeTabKey = activeTab.name,
|
||||||
menuOpen = menuOpen,
|
menuOpen = menuOpen,
|
||||||
chatsHighlighted = activeTab == AppTab.Talk && !talkInRoom,
|
chatsHighlighted = activeTab == AppTab.Talk && !talkInRoom,
|
||||||
navBackHighlighted = (activeTab == AppTab.Mail && mailSidebarOpen) ||
|
navBackHighlighted = (activeTab == AppTab.Mail && mailSidebarOpen) ||
|
||||||
@@ -499,36 +500,22 @@ fun AppScaffold(
|
|||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onCreateClick = {
|
onMailClick = {
|
||||||
when (activeTab) {
|
if (activeTab != AppTab.Mail) {
|
||||||
AppTab.Files -> {
|
pushTabHistory(activeTab)
|
||||||
if (F7AppPermissions.missing(context).isNotEmpty()) {
|
activeTab = AppTab.Mail
|
||||||
onRequestRuntimePermissions { filesUploadRequest++ }
|
|
||||||
} else {
|
|
||||||
filesUploadRequest++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AppTab.Contacts -> contactsCreateRequest++
|
|
||||||
AppTab.Tasks -> tasksCreateRequest++
|
|
||||||
AppTab.Support -> supportCreateRequest++
|
|
||||||
else -> Unit
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onProfileClick = {
|
onCardsClick = {
|
||||||
menuOpen = false
|
if (activeTab != AppTab.Deck) {
|
||||||
profileOpen = true
|
pushTabHistory(activeTab)
|
||||||
|
activeTab = AppTab.Deck
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onNotificationsClick = {
|
onConferencesClick = {
|
||||||
menuOpen = false
|
if (activeTab != AppTab.Talk) {
|
||||||
hasNotificationBadge = false
|
pushTabHistory(activeTab)
|
||||||
notificationsOpen = true
|
activeTab = AppTab.Talk
|
||||||
},
|
|
||||||
onSettingsClick = {
|
|
||||||
when (activeTab) {
|
|
||||||
AppTab.Mail -> mailSettingsOpen = true
|
|
||||||
AppTab.Calendar -> calendarSettingsRequest++
|
|
||||||
AppTab.Files -> filesSettingsOpen = true
|
|
||||||
else -> Unit
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onMenuClick = { menuOpen = !menuOpen },
|
onMenuClick = { menuOpen = !menuOpen },
|
||||||
@@ -637,12 +624,35 @@ fun AppScaffold(
|
|||||||
onItemClick = { index ->
|
onItemClick = { index ->
|
||||||
val item = appMenuItemsList.getOrNull(index) ?: return@F7AppMenuSheet
|
val item = appMenuItemsList.getOrNull(index) ?: return@F7AppMenuSheet
|
||||||
val external = item.externalUrl
|
val external = item.externalUrl
|
||||||
if (!external.isNullOrBlank()) {
|
val label = item.label.trim()
|
||||||
|
when {
|
||||||
|
// Профиль — нативная панель вместо веб-ЛК forbion
|
||||||
|
label.equals("Личный кабинет", ignoreCase = true) -> {
|
||||||
|
menuOpen = false
|
||||||
|
profileOpen = true
|
||||||
|
}
|
||||||
|
// Нативные пункты по дизайну меню
|
||||||
|
label.equals("Уведомления", ignoreCase = true) -> {
|
||||||
|
menuOpen = false
|
||||||
|
hasNotificationBadge = false
|
||||||
|
notificationsOpen = true
|
||||||
|
}
|
||||||
|
label.equals("Настройки", ignoreCase = true) -> {
|
||||||
|
menuOpen = false
|
||||||
|
when (activeTab) {
|
||||||
|
AppTab.Mail -> mailSettingsOpen = true
|
||||||
|
AppTab.Calendar -> calendarSettingsRequest++
|
||||||
|
AppTab.Files -> filesSettingsOpen = true
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
!external.isNullOrBlank() -> {
|
||||||
runCatching {
|
runCatching {
|
||||||
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(external)))
|
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(external)))
|
||||||
}
|
}
|
||||||
menuOpen = false
|
menuOpen = false
|
||||||
} else {
|
}
|
||||||
|
else -> {
|
||||||
appTabFromMenuIndex(index)?.let { tab ->
|
appTabFromMenuIndex(index)?.let { tab ->
|
||||||
if (tab != activeTab) {
|
if (tab != activeTab) {
|
||||||
pushTabHistory(activeTab)
|
pushTabHistory(activeTab)
|
||||||
@@ -651,6 +661,7 @@ fun AppScaffold(
|
|||||||
}
|
}
|
||||||
menuOpen = false
|
menuOpen = false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
ProfileSheet(
|
ProfileSheet(
|
||||||
@@ -679,6 +690,7 @@ fun AppScaffold(
|
|||||||
private const val PERMISSIONS_PROMPTED_KEY = "runtime_permissions_prompted_v2"
|
private const val PERMISSIONS_PROMPTED_KEY = "runtime_permissions_prompted_v2"
|
||||||
@Composable
|
@Composable
|
||||||
private fun LoginScreen(onLogin: (AuthSession) -> Unit) {
|
private fun LoginScreen(onLogin: (AuthSession) -> Unit) {
|
||||||
|
F7SecureScreen() // ввод пароля — не в скриншотах/recents
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
@@ -879,24 +891,6 @@ private fun LoginScreen(onLogin: (AuthSession) -> Unit) {
|
|||||||
submitLogin()
|
submitLogin()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
// Чекбокс trust-all показываем только в debug-сборках: в release он
|
|
||||||
// всё равно игнорируется на уровне сети/WebView (см. UnsafeSsl).
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Checkbox(
|
|
||||||
checked = trustAllCerts,
|
|
||||||
onCheckedChange = { trustAllCerts = it },
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "Доверять сертификату (dev)",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = F7Colors.TextSecondary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
F7PrimaryButton(
|
F7PrimaryButton(
|
||||||
text = if (loading) "…" else "Войти",
|
text = if (loading) "…" else "Войти",
|
||||||
|
|||||||
@@ -1,17 +1,38 @@
|
|||||||
package ru.forbion.f7cloud.mobile.ui
|
package ru.forbion.f7cloud.mobile.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
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.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Logout
|
||||||
|
import androidx.compose.material.icons.filled.Group
|
||||||
|
import androidx.compose.material.icons.filled.Person
|
||||||
|
import androidx.compose.material.icons.filled.QrCodeScanner
|
||||||
|
import androidx.compose.material.icons.filled.TaskAlt
|
||||||
|
import androidx.compose.material.icons.outlined.Info
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -21,22 +42,30 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
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.dp
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.Credentials
|
import okhttp3.Credentials
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.json.JSONObject
|
||||||
import ru.forbion.f7cloud.core.auth.AuthSession
|
import ru.forbion.f7cloud.core.auth.AuthSession
|
||||||
import ru.forbion.f7cloud.core.designsystem.F7Colors
|
import ru.forbion.f7cloud.core.designsystem.F7Colors
|
||||||
import ru.forbion.f7cloud.core.designsystem.F7FloatingPanel
|
import ru.forbion.f7cloud.core.designsystem.F7FloatingPanel
|
||||||
import ru.forbion.f7cloud.core.designsystem.F7NotificationRow
|
import ru.forbion.f7cloud.core.designsystem.F7NotificationRow
|
||||||
import ru.forbion.f7cloud.core.designsystem.F7PrimaryButton
|
|
||||||
import ru.forbion.f7cloud.core.designsystem.F7TextButton
|
|
||||||
import ru.forbion.f7cloud.core.designsystem.formatNotificationRelativeTime
|
import ru.forbion.f7cloud.core.designsystem.formatNotificationRelativeTime
|
||||||
import ru.forbion.f7cloud.core.network.F7Notification
|
import ru.forbion.f7cloud.core.network.F7Notification
|
||||||
|
import ru.forbion.f7cloud.core.network.NetworkFactory
|
||||||
import ru.forbion.f7cloud.core.network.NotificationsRepository
|
import ru.forbion.f7cloud.core.network.NotificationsRepository
|
||||||
import ru.forbion.f7cloud.core.network.UnauthorizedException
|
import ru.forbion.f7cloud.core.network.UnauthorizedException
|
||||||
|
import ru.forbion.f7cloud.core.network.applyOcsJson
|
||||||
|
|
||||||
private fun themeHeaderAsset(serverUrl: String, fileName: String): String =
|
private fun themeHeaderAsset(serverUrl: String, fileName: String): String =
|
||||||
"${serverUrl.trimEnd('/')}/themes/forbion/images/header/$fileName"
|
"${serverUrl.trimEnd('/')}/themes/forbion/images/header/$fileName"
|
||||||
@@ -54,48 +83,180 @@ fun ProfileSheet(
|
|||||||
onLogout: () -> Unit,
|
onLogout: () -> Unit,
|
||||||
onScanBrowserQr: () -> Unit = {},
|
onScanBrowserQr: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val base = session.serverUrl.trimEnd('/')
|
||||||
|
val userId = session.davUserId ?: session.username
|
||||||
|
var displayName by remember(session.username) { mutableStateOf(userId) }
|
||||||
|
var aboutOpen by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// Отображаемое имя тянем с сервера (OCS), пока — логин
|
||||||
|
LaunchedEffect(visible, session.username) {
|
||||||
|
if (visible) displayName = fetchDisplayName(session).ifBlank { userId }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openWeb(path: String) {
|
||||||
|
runCatching {
|
||||||
|
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("$base$path")))
|
||||||
|
}
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
|
||||||
F7FloatingPanel(
|
F7FloatingPanel(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
onDismiss = onDismiss,
|
onDismiss = onDismiss,
|
||||||
) {
|
) {
|
||||||
Text("Профиль", style = MaterialTheme.typography.titleMedium, color = F7Colors.TextPrimary)
|
// Шапка: аватар-инициал + имя/«Открыть профиль» + кнопка QR
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(44.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(F7Colors.PrimaryLight),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = session.username,
|
displayName.firstOrNull()?.uppercaseChar()?.toString() ?: "?",
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = F7Colors.TextPrimary,
|
fontWeight = FontWeight.SemiBold,
|
||||||
)
|
color = F7Colors.Primary,
|
||||||
Text(
|
|
||||||
text = session.serverUrl,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = F7Colors.TextSecondary,
|
|
||||||
)
|
|
||||||
session.davUserId?.let { davId ->
|
|
||||||
Text(
|
|
||||||
text = "ID: $davId",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = F7Colors.TextSecondary,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
F7PrimaryButton(
|
Column(
|
||||||
text = "Сканировать QR браузера",
|
modifier = Modifier
|
||||||
onClick = {
|
.weight(1f)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.clickable { openWeb("/u/$userId") }
|
||||||
|
.padding(vertical = 2.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
displayName,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = F7Colors.TextPrimary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Открыть профиль",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = F7Colors.Primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(RoundedCornerShape(10.dp))
|
||||||
|
.background(F7Colors.Grey2)
|
||||||
|
.clickable {
|
||||||
onDismiss()
|
onDismiss()
|
||||||
onScanBrowserQr()
|
onScanBrowserQr()
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.QrCodeScanner,
|
||||||
|
contentDescription = "Сканировать QR браузера",
|
||||||
|
tint = F7Colors.TextSecondary,
|
||||||
|
modifier = Modifier.size(22.dp),
|
||||||
)
|
)
|
||||||
F7PrimaryButton(
|
}
|
||||||
text = "Выйти",
|
}
|
||||||
onClick = {
|
Spacer(Modifier.height(10.dp))
|
||||||
|
HorizontalDivider(color = F7Colors.Border.copy(alpha = 0.5f))
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
ProfileMenuRow(Icons.Filled.TaskAlt, "Установить статус") { openWeb("/settings/user") }
|
||||||
|
ProfileMenuRow(Icons.Filled.Person, "Личные настройки") { openWeb("/settings/user") }
|
||||||
|
ProfileMenuRow(Icons.Filled.Group, "Учётные записи") { openWeb("/settings/user/security") }
|
||||||
|
ProfileMenuRow(Icons.Outlined.Info, "О программе и что нового") { aboutOpen = true }
|
||||||
|
ProfileMenuRow(
|
||||||
|
icon = Icons.AutoMirrored.Filled.Logout,
|
||||||
|
label = "Выйти",
|
||||||
|
tint = F7Colors.Error,
|
||||||
|
) {
|
||||||
onDismiss()
|
onDismiss()
|
||||||
onLogout()
|
onLogout()
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
F7TextButton(text = "Закрыть", onClick = onDismiss, modifier = Modifier.fillMaxWidth())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (aboutOpen) {
|
||||||
|
F7AboutDialog(onDismiss = { aboutOpen = false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ProfileMenuRow(
|
||||||
|
icon: ImageVector,
|
||||||
|
label: String,
|
||||||
|
tint: Color = F7Colors.TextPrimary,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(10.dp))
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(vertical = 12.dp, horizontal = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
|
) {
|
||||||
|
Icon(icon, contentDescription = null, tint = tint, modifier = Modifier.size(22.dp))
|
||||||
|
Text(label, style = MaterialTheme.typography.bodyLarge, color = tint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun F7AboutDialog(onDismiss: () -> Unit) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val version = remember {
|
||||||
|
runCatching {
|
||||||
|
context.packageManager.getPackageInfo(context.packageName, 0).versionName
|
||||||
|
}.getOrNull() ?: "—"
|
||||||
|
}
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = onDismiss) { Text("Закрыть") }
|
||||||
|
},
|
||||||
|
title = { Text("F7cloud Mobile") },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
Text("Версия $version", style = MaterialTheme.typography.bodyMedium, color = F7Colors.TextPrimary)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
"Нативный клиент F7cloud: почта, файлы, календарь, контакты, задачи, конференции, поддержка.",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = F7Colors.TextSecondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Отображаемое имя пользователя из OCS (fallback — пусто). */
|
||||||
|
private suspend fun fetchDisplayName(session: AuthSession): String = withContext(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
val client = NetworkFactory.newAuthedClient(
|
||||||
|
session.username,
|
||||||
|
session.appPassword,
|
||||||
|
session.trustAllCerts,
|
||||||
|
)
|
||||||
|
val url = "${session.serverUrl.trimEnd('/')}/ocs/v2.php/cloud/user?format=json"
|
||||||
|
val request = Request.Builder().url(url).applyOcsJson().get().build()
|
||||||
|
client.newCall(request).execute().use { response ->
|
||||||
|
val body = response.body?.string().orEmpty()
|
||||||
|
JSONObject(body)
|
||||||
|
.optJSONObject("ocs")
|
||||||
|
?.optJSONObject("data")
|
||||||
|
?.optString("displayname")
|
||||||
|
.orEmpty()
|
||||||
|
}
|
||||||
|
}.getOrDefault("")
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NotificationsSheet(
|
fun NotificationsSheet(
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
|
|||||||
+79
-8
@@ -9,10 +9,12 @@ import androidx.compose.animation.slideOutVertically
|
|||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -23,8 +25,10 @@ import androidx.compose.foundation.layout.width
|
|||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -35,8 +39,10 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -51,11 +57,18 @@ private val BottomBarReserve: Dp = 90.dp
|
|||||||
private val MenuIconSize = 62.dp
|
private val MenuIconSize = 62.dp
|
||||||
private val MenuGridGap = 20.dp
|
private val MenuGridGap = 20.dp
|
||||||
|
|
||||||
|
// Меню-лист выезжает снизу и занимает ~пол-экрана (не весь), по требованию владельца
|
||||||
|
private const val MenuSheetHeightFraction = 0.6f
|
||||||
|
private val MenuSheetShape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)
|
||||||
|
|
||||||
data class F7AppMenuItem(
|
data class F7AppMenuItem(
|
||||||
val label: String,
|
val label: String,
|
||||||
val iconUrl: String,
|
val iconUrl: String,
|
||||||
val selected: Boolean,
|
val selected: Boolean,
|
||||||
val externalUrl: String? = null,
|
val externalUrl: String? = null,
|
||||||
|
// Локальная иконка вместо серверной (для нативных пунктов — Уведомления/Настройки),
|
||||||
|
// рисуется в круглом бейдже с зелёной обводкой под стиль glass-иконок.
|
||||||
|
val localIcon: ImageVector? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -78,6 +91,26 @@ fun F7AppMenuSheet(
|
|||||||
}
|
}
|
||||||
val base = serverUrl.trimEnd('/')
|
val base = serverUrl.trimEnd('/')
|
||||||
|
|
||||||
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
|
// Затемнение над листом — тап закрывает меню (лист занимает только низ экрана)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = visible,
|
||||||
|
enter = fadeIn(tween(200)),
|
||||||
|
exit = fadeOut(tween(200)),
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.18f))
|
||||||
|
.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null,
|
||||||
|
onClick = onDismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Сам лист — выезжает снизу вверх на ~пол-экрана, скруглённый верх + тень
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
enter = fadeIn(tween(250)) + slideInVertically(
|
enter = fadeIn(tween(250)) + slideInVertically(
|
||||||
@@ -88,30 +121,45 @@ fun F7AppMenuSheet(
|
|||||||
animationSpec = tween(300),
|
animationSpec = tween(300),
|
||||||
targetOffsetY = { it },
|
targetOffsetY = { it },
|
||||||
),
|
),
|
||||||
modifier = modifier,
|
modifier = Modifier.align(Alignment.BottomCenter),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(MenuSheetHeightFraction)
|
||||||
.padding(bottom = BottomBarReserve)
|
.padding(bottom = BottomBarReserve)
|
||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
|
.shadow(16.dp, MenuSheetShape, clip = false)
|
||||||
|
.clip(MenuSheetShape)
|
||||||
.background(F7Colors.Background)
|
.background(F7Colors.Background)
|
||||||
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp),
|
.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp),
|
||||||
) {
|
) {
|
||||||
|
// Ручка-хендл
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(top = 2.dp, bottom = 10.dp)
|
||||||
|
.width(40.dp)
|
||||||
|
.height(4.dp)
|
||||||
|
.clip(RoundedCornerShape(2.dp))
|
||||||
|
.background(F7Colors.Border),
|
||||||
|
)
|
||||||
F7AppMenuSearchField(
|
F7AppMenuSearchField(
|
||||||
serverUrl = base,
|
serverUrl = base,
|
||||||
value = searchQuery,
|
value = searchQuery,
|
||||||
onValueChange = { searchQuery = it },
|
onValueChange = { searchQuery = it },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(bottom = 24.dp),
|
.padding(bottom = 20.dp),
|
||||||
)
|
)
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Fixed(4),
|
columns = GridCells.Fixed(3),
|
||||||
horizontalArrangement = Arrangement.spacedBy(MenuGridGap),
|
horizontalArrangement = Arrangement.spacedBy(MenuGridGap),
|
||||||
verticalArrangement = Arrangement.spacedBy(MenuGridGap),
|
verticalArrangement = Arrangement.spacedBy(MenuGridGap),
|
||||||
contentPadding = PaddingValues(horizontal = 2.dp),
|
contentPadding = PaddingValues(horizontal = 2.dp),
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
items = filteredItems,
|
items = filteredItems,
|
||||||
@@ -136,6 +184,7 @@ fun F7AppMenuSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun F7AppMenuSearchField(
|
private fun F7AppMenuSearchField(
|
||||||
@@ -195,17 +244,39 @@ private fun F7AppMenuGridItem(
|
|||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(MenuIconSize)
|
.fillMaxWidth()
|
||||||
.clickable(onClick = onClick),
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(vertical = 4.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
|
val localIcon = item.localIcon
|
||||||
|
if (localIcon != null) {
|
||||||
|
// Нативный пункт: иконка в круглом бейдже с зелёной обводкой (стиль glass)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(MenuIconSize)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color.White)
|
||||||
|
.border(1.5.dp, F7Colors.Green30, CircleShape),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
localIcon,
|
||||||
|
contentDescription = item.label,
|
||||||
|
tint = F7Colors.Primary,
|
||||||
|
modifier = Modifier.size(MenuIconSize * 0.46f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = item.iconUrl,
|
model = item.iconUrl,
|
||||||
contentDescription = item.label,
|
contentDescription = item.label,
|
||||||
modifier = Modifier.size(MenuIconSize),
|
modifier = Modifier.size(MenuIconSize),
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text = item.label,
|
text = item.label,
|
||||||
style = MaterialTheme.typography.labelLarge.copy(
|
style = MaterialTheme.typography.labelLarge.copy(
|
||||||
|
|||||||
+12
-63
@@ -3,10 +3,9 @@ package ru.forbion.f7cloud.core.designsystem
|
|||||||
enum class F7BottomBarSlot {
|
enum class F7BottomBarSlot {
|
||||||
Chats,
|
Chats,
|
||||||
NavBack,
|
NavBack,
|
||||||
Create,
|
SectionMail,
|
||||||
Profile,
|
SectionCards,
|
||||||
Notifications,
|
SectionConferences,
|
||||||
Settings,
|
|
||||||
Menu,
|
Menu,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,72 +15,22 @@ data class F7BottomBarConfig(
|
|||||||
val buttonCount: Int get() = slots.size
|
val buttonCount: Int get() = slots.size
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
// Единая навигационная панель по дизайну «Новое меню»:
|
||||||
|
// слева «назад», затем ярлыки-разделы Почта · Карточки · Конференции,
|
||||||
|
// справа — выдвижное меню. Кнопки «чаты» (по просьбе владельца убрана),
|
||||||
|
// профиль/уведомления/настройки/создать в панели нет (меню и шапки экранов).
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
fun forContext(
|
fun forContext(
|
||||||
tabKey: String,
|
tabKey: String,
|
||||||
talkInRoom: Boolean,
|
talkInRoom: Boolean,
|
||||||
): F7BottomBarConfig = when (tabKey) {
|
): F7BottomBarConfig = F7BottomBarConfig(
|
||||||
"Talk" -> if (talkInRoom) {
|
|
||||||
F7BottomBarConfig(listOf(F7BottomBarSlot.Profile, F7BottomBarSlot.Notifications, F7BottomBarSlot.Menu))
|
|
||||||
} else {
|
|
||||||
F7BottomBarConfig(
|
|
||||||
listOf(
|
|
||||||
F7BottomBarSlot.Chats,
|
|
||||||
F7BottomBarSlot.Profile,
|
|
||||||
F7BottomBarSlot.Notifications,
|
|
||||||
F7BottomBarSlot.Menu,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
"Files" -> F7BottomBarConfig(
|
|
||||||
listOf(
|
listOf(
|
||||||
F7BottomBarSlot.NavBack,
|
F7BottomBarSlot.NavBack,
|
||||||
F7BottomBarSlot.Profile,
|
F7BottomBarSlot.SectionMail,
|
||||||
F7BottomBarSlot.Notifications,
|
F7BottomBarSlot.SectionCards,
|
||||||
F7BottomBarSlot.Create,
|
F7BottomBarSlot.SectionConferences,
|
||||||
F7BottomBarSlot.Settings,
|
|
||||||
F7BottomBarSlot.Menu,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
"Contacts" -> F7BottomBarConfig(
|
|
||||||
listOf(
|
|
||||||
F7BottomBarSlot.Create,
|
|
||||||
F7BottomBarSlot.Profile,
|
|
||||||
F7BottomBarSlot.Notifications,
|
|
||||||
F7BottomBarSlot.Menu,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
"Tasks" -> F7BottomBarConfig(
|
|
||||||
listOf(
|
|
||||||
F7BottomBarSlot.Create,
|
|
||||||
F7BottomBarSlot.Profile,
|
|
||||||
F7BottomBarSlot.Notifications,
|
|
||||||
F7BottomBarSlot.Menu,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
"Support" -> F7BottomBarConfig(
|
|
||||||
listOf(
|
|
||||||
F7BottomBarSlot.Create,
|
|
||||||
F7BottomBarSlot.Profile,
|
|
||||||
F7BottomBarSlot.Notifications,
|
|
||||||
F7BottomBarSlot.Menu,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
"Mail", "Calendar" -> F7BottomBarConfig(
|
|
||||||
listOf(
|
|
||||||
F7BottomBarSlot.NavBack,
|
|
||||||
F7BottomBarSlot.Profile,
|
|
||||||
F7BottomBarSlot.Notifications,
|
|
||||||
F7BottomBarSlot.Settings,
|
|
||||||
F7BottomBarSlot.Menu,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else -> F7BottomBarConfig(
|
|
||||||
listOf(
|
|
||||||
F7BottomBarSlot.Profile,
|
|
||||||
F7BottomBarSlot.Notifications,
|
|
||||||
F7BottomBarSlot.Menu,
|
F7BottomBarSlot.Menu,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
+4
-1
@@ -570,7 +570,10 @@ fun F7AppScaffold(
|
|||||||
content(
|
content(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.f7SafeTopInsets(),
|
// Отступы под статус-бар (сверху) И системную навигацию (снизу),
|
||||||
|
// иначе на edge-to-edge (targetSdk 36) контент залезает под панели.
|
||||||
|
.f7SafeTopInsets()
|
||||||
|
.f7SafeBottomInsets(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
|
|||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
package ru.forbion.f7cloud.core.designsystem
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Зелёная круглая кнопка «+» — единый элемент создания в шапках экранов
|
||||||
|
* (Почта/Файлы/Контакты/…). Градиент Primary, как у других зелёных кнопок.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun F7CreateButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
size: Dp = 44.dp,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(size)
|
||||||
|
.shadow(3.dp, CircleShape, spotColor = F7Colors.Primary.copy(alpha = 0.25f))
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
Brush.linearGradient(
|
||||||
|
listOf(F7Colors.PrimaryGradientStart, F7Colors.PrimaryGradientEnd),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.border(1.dp, F7Colors.Primary.copy(alpha = 0.22f), CircleShape)
|
||||||
|
.clickable(onClick = onClick),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "+",
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 26.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
-39
@@ -21,21 +21,25 @@ import androidx.compose.ui.graphics.Brush
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
|
||||||
data class F7BottomBarActions(
|
data class F7BottomBarActions(
|
||||||
val onChatsClick: () -> Unit = {},
|
val onChatsClick: () -> Unit = {},
|
||||||
val onNavBackClick: () -> Unit = {},
|
val onNavBackClick: () -> Unit = {},
|
||||||
val onCreateClick: () -> Unit = {},
|
val onMailClick: () -> Unit = {},
|
||||||
val onProfileClick: () -> Unit = {},
|
val onCardsClick: () -> Unit = {},
|
||||||
val onNotificationsClick: () -> Unit = {},
|
val onConferencesClick: () -> Unit = {},
|
||||||
val onSettingsClick: () -> Unit = {},
|
|
||||||
val onMenuClick: () -> Unit = {},
|
val onMenuClick: () -> Unit = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
private val BottomBarButtonSize = 55.dp
|
private val BottomBarButtonSize = 55.dp
|
||||||
private val BottomBarIconSize = 24.dp
|
private val BottomBarIconSize = 34.dp
|
||||||
|
|
||||||
|
// Иконки-разделы (Почта/Карточки/Конференции) — серверные SVG с внутренним
|
||||||
|
// «воздухом», поэтому рендерим крупнее, чтобы заполняли кружок.
|
||||||
|
private val BottomBarSectionIconSize = 46.dp
|
||||||
private val BottomBarGap = 8.dp
|
private val BottomBarGap = 8.dp
|
||||||
private val BottomBarOuterPaddingH = 6.dp
|
private val BottomBarOuterPaddingH = 6.dp
|
||||||
private val BottomBarOuterPaddingV = 6.dp
|
private val BottomBarOuterPaddingV = 6.dp
|
||||||
@@ -43,9 +47,6 @@ private val BottomBarButtonShape = RoundedCornerShape(100.dp)
|
|||||||
private val BottomBarBorderBrush = Brush.linearGradient(
|
private val BottomBarBorderBrush = Brush.linearGradient(
|
||||||
listOf(Color(0x33C0FF7B), Color(0x3370B62B)),
|
listOf(Color(0x33C0FF7B), Color(0x3370B62B)),
|
||||||
)
|
)
|
||||||
private val BottomBarHighlightBrush = Brush.linearGradient(
|
|
||||||
listOf(Color(0x33C0FF7B), Color(0x3370B62B)),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun F7MobileBottomBar(
|
fun F7MobileBottomBar(
|
||||||
@@ -53,6 +54,7 @@ fun F7MobileBottomBar(
|
|||||||
userId: String,
|
userId: String,
|
||||||
config: F7BottomBarConfig,
|
config: F7BottomBarConfig,
|
||||||
actions: F7BottomBarActions,
|
actions: F7BottomBarActions,
|
||||||
|
activeTabKey: String = "",
|
||||||
menuOpen: Boolean = false,
|
menuOpen: Boolean = false,
|
||||||
chatsHighlighted: Boolean = false,
|
chatsHighlighted: Boolean = false,
|
||||||
navBackHighlighted: Boolean = false,
|
navBackHighlighted: Boolean = false,
|
||||||
@@ -87,36 +89,31 @@ fun F7MobileBottomBar(
|
|||||||
"$base/themes/forbion/images/header/chat-icon-gray.svg"
|
"$base/themes/forbion/images/header/chat-icon-gray.svg"
|
||||||
},
|
},
|
||||||
contentDescription = "Чаты",
|
contentDescription = "Чаты",
|
||||||
highlighted = chatsHighlighted,
|
|
||||||
onClick = actions.onChatsClick,
|
onClick = actions.onChatsClick,
|
||||||
)
|
)
|
||||||
F7BottomBarSlot.NavBack -> F7BottomBarIconSlot(
|
F7BottomBarSlot.NavBack -> F7BottomBarIconSlot(
|
||||||
iconUrl = "$base/themes/forbion/images/header/sidebar-chevron-left.svg",
|
iconUrl = "$base/themes/forbion/images/header/sidebar-chevron-left.svg",
|
||||||
contentDescription = "Папки",
|
contentDescription = "Папки",
|
||||||
highlighted = navBackHighlighted,
|
|
||||||
iconRotation = if (navBackHighlighted) 180f else 0f,
|
iconRotation = if (navBackHighlighted) 180f else 0f,
|
||||||
onClick = actions.onNavBackClick,
|
onClick = actions.onNavBackClick,
|
||||||
)
|
)
|
||||||
F7BottomBarSlot.Create -> F7BottomBarIconSlot(
|
F7BottomBarSlot.SectionMail -> F7BottomBarIconSlot(
|
||||||
iconUrl = "$base/themes/forbion/images/header/green-plus.svg",
|
iconUrl = "$base/themes/forbion/images/header/mail-header-icon.svg",
|
||||||
contentDescription = "Создать",
|
contentDescription = "Почта",
|
||||||
onClick = actions.onCreateClick,
|
iconSize = BottomBarSectionIconSize,
|
||||||
|
onClick = actions.onMailClick,
|
||||||
)
|
)
|
||||||
F7BottomBarSlot.Profile -> F7BottomBarIconSlot(
|
F7BottomBarSlot.SectionCards -> F7BottomBarIconSlot(
|
||||||
iconUrl = "$base/themes/forbion/images/header/profile-menu-icon-big.svg",
|
iconUrl = "$base/themes/forbion/images/header/deck-header-icon.svg",
|
||||||
contentDescription = "Профиль",
|
contentDescription = "Карточки",
|
||||||
onClick = actions.onProfileClick,
|
iconSize = BottomBarSectionIconSize,
|
||||||
|
onClick = actions.onCardsClick,
|
||||||
)
|
)
|
||||||
F7BottomBarSlot.Notifications -> F7BottomBarIconSlot(
|
F7BottomBarSlot.SectionConferences -> F7BottomBarIconSlot(
|
||||||
iconUrl = "$base/themes/forbion/images/header/not-menu-icon-big.svg",
|
iconUrl = "$base/themes/forbion/images/header/spreed-header-icon.svg",
|
||||||
contentDescription = "Уведомления",
|
contentDescription = "Конференции",
|
||||||
showBadge = showNotificationBadge,
|
iconSize = BottomBarSectionIconSize,
|
||||||
onClick = actions.onNotificationsClick,
|
onClick = actions.onConferencesClick,
|
||||||
)
|
|
||||||
F7BottomBarSlot.Settings -> F7BottomBarIconSlot(
|
|
||||||
iconUrl = "$base/themes/forbion/images/header/setting-menu-icon.svg",
|
|
||||||
contentDescription = "Настройки",
|
|
||||||
onClick = actions.onSettingsClick,
|
|
||||||
)
|
)
|
||||||
F7BottomBarSlot.Menu -> F7BottomBarIconSlot(
|
F7BottomBarSlot.Menu -> F7BottomBarIconSlot(
|
||||||
iconUrl = if (menuOpen) {
|
iconUrl = if (menuOpen) {
|
||||||
@@ -125,7 +122,7 @@ fun F7MobileBottomBar(
|
|||||||
"$base/themes/forbion/images/header/menu-burger-gray.svg"
|
"$base/themes/forbion/images/header/menu-burger-gray.svg"
|
||||||
},
|
},
|
||||||
contentDescription = "Меню",
|
contentDescription = "Меню",
|
||||||
highlighted = menuOpen,
|
showBadge = showNotificationBadge,
|
||||||
onClick = actions.onMenuClick,
|
onClick = actions.onMenuClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -138,22 +135,16 @@ private fun F7BottomBarIconSlot(
|
|||||||
iconUrl: String,
|
iconUrl: String,
|
||||||
contentDescription: String,
|
contentDescription: String,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
highlighted: Boolean = false,
|
|
||||||
iconRotation: Float = 0f,
|
iconRotation: Float = 0f,
|
||||||
showBadge: Boolean = false,
|
showBadge: Boolean = false,
|
||||||
|
iconSize: Dp = BottomBarIconSize,
|
||||||
) {
|
) {
|
||||||
val bg = if (highlighted) BottomBarHighlightBrush else null
|
// Зелёной подложки под активной кнопкой быть не должно — фон всегда нейтральный.
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(BottomBarButtonSize)
|
.size(BottomBarButtonSize)
|
||||||
.clip(BottomBarButtonShape)
|
.clip(BottomBarButtonShape)
|
||||||
.then(
|
.background(Color(0x99FFFFFF), BottomBarButtonShape)
|
||||||
if (bg != null) {
|
|
||||||
Modifier.background(bg, BottomBarButtonShape)
|
|
||||||
} else {
|
|
||||||
Modifier.background(Color(0x99FFFFFF), BottomBarButtonShape)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.border(
|
.border(
|
||||||
width = 1.dp,
|
width = 1.dp,
|
||||||
brush = BottomBarBorderBrush,
|
brush = BottomBarBorderBrush,
|
||||||
@@ -170,7 +161,7 @@ private fun F7BottomBarIconSlot(
|
|||||||
model = iconUrl,
|
model = iconUrl,
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(BottomBarIconSize)
|
.size(iconSize)
|
||||||
.graphicsLayer { rotationZ = iconRotation },
|
.graphicsLayer { rotationZ = iconRotation },
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
)
|
)
|
||||||
|
|||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package ru.forbion.f7cloud.core.designsystem
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.view.WindowManager
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Пока этот экран на виду — окно помечено FLAG_SECURE: содержимое не попадает
|
||||||
|
* в скриншоты, превью в «недавних» и запись экрана. Вешать на чувствительные
|
||||||
|
* экраны (ввод пароля/PIN). Флаг снимается при уходе с экрана.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun F7SecureScreen() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
val window = (context as? Activity)?.window
|
||||||
|
window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||||
|
onDispose {
|
||||||
|
window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
-7
@@ -17,13 +17,20 @@ Gradle-кэшем, а не контейнер (иначе WebRTC качаетс
|
|||||||
как у билд-машины: **JDK 21** (`/usr/lib/jvm/java-21-openjdk-amd64`) + **Android SDK 36**
|
как у билд-машины: **JDK 21** (`/usr/lib/jvm/java-21-openjdk-amd64`) + **Android SDK 36**
|
||||||
(`/opt/android-sdk`, platform android-36, build-tools 36.0.0).
|
(`/opt/android-sdk`, platform android-36, build-tools 36.0.0).
|
||||||
|
|
||||||
Регистрация (нужен registration-токен из Gitea → Settings → Actions → Runners, **права Gitea-админа**):
|
Регистрация — **repo-токен** из Gitea → репозиторий `root/f7cloud_mobile` → Settings → Actions →
|
||||||
|
Runners → Create new Runner (НЕ site-admin токен — тот регистрирует раннер общеинстансно и он
|
||||||
|
подхватывает джобы чужих ячеек; проверено — брал `glb_adm/f7_talk`). Метки `self-hosted`+`f7-android`
|
||||||
|
соответствуют `runs-on: [self-hosted, f7-android]`.
|
||||||
|
|
||||||
|
**Развёрнуто на инфре B (2026-07-09):** `act_runner` v0.2.11 + Node 20 (`/opt/nodejs`) + Docker
|
||||||
|
(`docker.io`, нужен act_runner для старта, но джобы идут на **хосте** — метки `:host`, тёплый кэш
|
||||||
|
`/root/.gradle`). Конфиг `/opt/act_runner/config.yaml` (labels `self-hosted:host`,`f7-android:host`,
|
||||||
|
`container.docker_host: "-"`). systemd-сервис `act_runner.service` (enabled, Restart=always,
|
||||||
|
env JAVA_HOME/ANDROID_HOME/PATH+node). Регистрация — repo-scope (только f7cloud_mobile).
|
||||||
```bash
|
```bash
|
||||||
# на машине раннера (инфра B):
|
systemctl status act_runner # состояние
|
||||||
act_runner register --instance https://git.f7cloud.ru --token <TOKEN> --labels f7-android
|
journalctl -u act_runner -f # логи
|
||||||
act_runner daemon
|
|
||||||
```
|
```
|
||||||
Метка `f7-android` соответствует `runs-on: [self-hosted, f7-android]` в workflow.
|
|
||||||
|
|
||||||
## Secrets репозитория (Gitea → Settings → Actions → Secrets)
|
## Secrets репозитория (Gitea → Settings → Actions → Secrets)
|
||||||
|
|
||||||
@@ -43,5 +50,7 @@ scripts/package-source.sh dist/ # → dist/f7cloud-mobile-v<ver>-<rev>-sourc
|
|||||||
|
|
||||||
## Статус
|
## Статус
|
||||||
|
|
||||||
Workflow и скрипт готовы и в репо. **Не активны, пока не зарегистрирован раннер** (инфра-шаг ячейки B,
|
**Активно (2026-07-09):** self-hosted раннер `f7-b-android` зарегистрирован и запущен на инфре B
|
||||||
нужен Gitea-админ для токена). До активации: сборки — вручную на билд-машине, исходники — скриптом.
|
(scope — репозиторий, host-исполнение). Workflow срабатывает на push/PR (lint+тесты+assembleDebug).
|
||||||
|
Осталось для джобы `release` по тегу: завести Secrets репозитория (keystore base64 + пароли) —
|
||||||
|
пока release-сборки делаются вручную на билд-машине тем же ключом.
|
||||||
|
|||||||
+71
-53
@@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -388,46 +387,55 @@ private fun CalendarIconButton(
|
|||||||
fun CalendarMonthGrid(
|
fun CalendarMonthGrid(
|
||||||
month: YearMonth,
|
month: YearMonth,
|
||||||
selectedDay: LocalDate,
|
selectedDay: LocalDate,
|
||||||
daysWithEvents: Set<LocalDate>,
|
|
||||||
eventsByDay: Map<LocalDate, List<CalendarEventItem>>,
|
eventsByDay: Map<LocalDate, List<CalendarEventItem>>,
|
||||||
onSelectDay: (LocalDate) -> Unit,
|
onSelectDay: (LocalDate) -> Unit,
|
||||||
) {
|
) {
|
||||||
val days = CalendarRepository.monthGridDays(month)
|
val days = CalendarRepository.monthGridDays(month)
|
||||||
val today = LocalDate.now()
|
val today = LocalDate.now()
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.fillMaxWidth()
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
.clip(RoundedCornerShape(12.dp))
|
) {
|
||||||
.background(F7Colors.Surface)
|
// Заголовок дней недели — серые пилюли (по макету «Календарь · Месяц»)
|
||||||
.border(1.dp, F7Colors.Border, RoundedCornerShape(12.dp))
|
Row(
|
||||||
.padding(8.dp),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
weekDayLabels.forEach { label ->
|
weekDayLabels.forEach { label ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.clip(RoundedCornerShape(100.dp))
|
||||||
|
.background(F7Colors.Grey2)
|
||||||
|
.padding(vertical = 6.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = label,
|
text = label,
|
||||||
modifier = Modifier.weight(1f),
|
style = MaterialTheme.typography.labelMedium,
|
||||||
textAlign = TextAlign.Center,
|
fontWeight = FontWeight.Medium,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
color = F7Colors.TextSecondary,
|
||||||
color = F7Colors.TextMuted,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// Гридлайн-сетка без скруглений (по макету)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.border(0.5.dp, F7Colors.Grey3),
|
||||||
|
) {
|
||||||
days.chunked(7).forEach { week ->
|
days.chunked(7).forEach { week ->
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
week.forEach { day ->
|
week.forEach { day ->
|
||||||
val inMonth = day.month == month.month
|
val inMonth = day.month == month.month
|
||||||
val selected = day == selectedDay
|
|
||||||
val hasEvents = daysWithEvents.contains(day)
|
|
||||||
val dayEvents = eventsByDay[day].orEmpty()
|
val dayEvents = eventsByDay[day].orEmpty()
|
||||||
CalendarDayCell(
|
CalendarDayCell(
|
||||||
day = day.dayOfMonth,
|
day = day.dayOfMonth,
|
||||||
inMonth = inMonth,
|
inMonth = inMonth,
|
||||||
isToday = day == today,
|
isToday = day == today,
|
||||||
selected = selected,
|
selected = day == selectedDay,
|
||||||
hasEvents = hasEvents,
|
events = dayEvents,
|
||||||
preview = dayEvents.firstOrNull()?.summary.orEmpty(),
|
|
||||||
onClick = { onSelectDay(day) },
|
onClick = { onSelectDay(day) },
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
@@ -436,6 +444,7 @@ fun CalendarMonthGrid(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CalendarDayCell(
|
private fun CalendarDayCell(
|
||||||
@@ -443,63 +452,72 @@ private fun CalendarDayCell(
|
|||||||
inMonth: Boolean,
|
inMonth: Boolean,
|
||||||
isToday: Boolean,
|
isToday: Boolean,
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
hasEvents: Boolean,
|
events: List<CalendarEventItem>,
|
||||||
preview: String,
|
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val bg = when {
|
val cellBg = when {
|
||||||
|
isToday -> F7Colors.Green10
|
||||||
selected -> F7Colors.PrimaryLight
|
selected -> F7Colors.PrimaryLight
|
||||||
isToday -> F7Colors.PrimaryLight.copy(alpha = 0.55f)
|
|
||||||
else -> F7Colors.Surface
|
else -> F7Colors.Surface
|
||||||
}
|
}
|
||||||
val borderColor = when {
|
Column(
|
||||||
selected -> F7Colors.Primary
|
|
||||||
isToday -> F7Colors.PrimaryDark
|
|
||||||
else -> F7Colors.BorderLight
|
|
||||||
}
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.aspectRatio(1f)
|
.heightIn(min = 66.dp)
|
||||||
.padding(2.dp)
|
.border(0.5.dp, F7Colors.Grey3)
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.background(cellBg)
|
||||||
.background(bg)
|
|
||||||
.border(1.dp, borderColor, RoundedCornerShape(8.dp))
|
|
||||||
.clickable(onClick = onClick)
|
.clickable(onClick = onClick)
|
||||||
.padding(2.dp),
|
.padding(horizontal = 4.dp, vertical = 4.dp),
|
||||||
contentAlignment = Alignment.TopCenter,
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
) {
|
||||||
|
// Номер дня сверху-слева; сегодня — в зелёном круге
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(22.dp)
|
||||||
|
.then(if (isToday) Modifier.clip(CircleShape).background(F7Colors.Primary) else Modifier),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
||||||
Text(
|
Text(
|
||||||
text = day.toString(),
|
text = day.toString(),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
fontWeight = if (isToday) FontWeight.Bold else FontWeight.Normal,
|
fontWeight = if (isToday) FontWeight.SemiBold else FontWeight.Normal,
|
||||||
color = when {
|
color = when {
|
||||||
|
isToday -> F7Colors.TextOnPrimary
|
||||||
!inMonth -> F7Colors.TextMuted
|
!inMonth -> F7Colors.TextMuted
|
||||||
selected -> F7Colors.PrimaryDark
|
|
||||||
else -> F7Colors.TextPrimary
|
else -> F7Colors.TextPrimary
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if (hasEvents && inMonth && preview.isNotBlank()) {
|
}
|
||||||
|
if (inMonth) {
|
||||||
|
events.take(2).forEach { event ->
|
||||||
|
CalendarDayEventPill(event.summary.ifBlank { "Событие" })
|
||||||
|
}
|
||||||
|
if (events.size > 2) {
|
||||||
Text(
|
Text(
|
||||||
preview,
|
"+${events.size - 2}",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = F7Colors.Primary,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CalendarDayEventPill(text: String) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
.background(F7Colors.PrimaryLight)
|
||||||
|
.padding(horizontal = 4.dp, vertical = 1.dp),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = F7Colors.PrimaryDark,
|
color = F7Colors.PrimaryDark,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.padding(top = 1.dp),
|
|
||||||
)
|
)
|
||||||
} else if (hasEvents && inMonth) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(top = 2.dp)
|
|
||||||
.size(5.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(F7Colors.Primary),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -343,7 +343,6 @@ fun CalendarScreen(
|
|||||||
CalendarMonthGrid(
|
CalendarMonthGrid(
|
||||||
month = state.visibleMonth,
|
month = state.visibleMonth,
|
||||||
selectedDay = state.selectedDay,
|
selectedDay = state.selectedDay,
|
||||||
daysWithEvents = vm.daysWithEvents(),
|
|
||||||
eventsByDay = eventsByDay,
|
eventsByDay = eventsByDay,
|
||||||
onSelectDay = vm::selectDay,
|
onSelectDay = vm::selectDay,
|
||||||
)
|
)
|
||||||
|
|||||||
+15
-3
@@ -48,6 +48,7 @@ import coil.compose.AsyncImage
|
|||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import ru.forbion.f7cloud.core.auth.AuthSession
|
import ru.forbion.f7cloud.core.auth.AuthSession
|
||||||
import ru.forbion.f7cloud.core.designsystem.F7Colors
|
import ru.forbion.f7cloud.core.designsystem.F7Colors
|
||||||
|
import ru.forbion.f7cloud.core.designsystem.F7CreateButton
|
||||||
import ru.forbion.f7cloud.core.designsystem.F7ModuleScreen
|
import ru.forbion.f7cloud.core.designsystem.F7ModuleScreen
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -75,12 +76,24 @@ fun ContactsScreen(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
loading = state.loading && state.contacts.isEmpty(),
|
loading = state.loading && state.contacts.isEmpty(),
|
||||||
error = state.error,
|
error = state.error,
|
||||||
|
) {
|
||||||
|
// Поиск + кнопка создания контакта (создание раньше вызывалось из нижней
|
||||||
|
// панели, теперь — из шапки, панель стала навигационной)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
ContactsSearchBar(
|
ContactsSearchBar(
|
||||||
serverUrl = session.serverUrl,
|
serverUrl = session.serverUrl,
|
||||||
query = state.searchQuery,
|
query = state.searchQuery,
|
||||||
onQueryChange = vm::setSearchQuery,
|
onQueryChange = vm::setSearchQuery,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
|
F7CreateButton(onClick = vm::openAddSheet, size = 40.dp)
|
||||||
|
}
|
||||||
if (state.syncing && state.contacts.isNotEmpty()) {
|
if (state.syncing && state.contacts.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
"Обновление…",
|
"Обновление…",
|
||||||
@@ -297,12 +310,11 @@ private fun ContactsSearchBar(
|
|||||||
serverUrl: String,
|
serverUrl: String,
|
||||||
query: String,
|
query: String,
|
||||||
onQueryChange: (String) -> Unit,
|
onQueryChange: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val base = serverUrl.trimEnd('/')
|
val base = serverUrl.trimEnd('/')
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(bottom = 8.dp)
|
|
||||||
.height(40.dp)
|
.height(40.dp)
|
||||||
.shadow(2.dp, RoundedCornerShape(100.dp), spotColor = Color(0xFFCBCBCB))
|
.shadow(2.dp, RoundedCornerShape(100.dp), spotColor = Color(0xFFCBCBCB))
|
||||||
.clip(RoundedCornerShape(100.dp))
|
.clip(RoundedCornerShape(100.dp))
|
||||||
|
|||||||
@@ -21,9 +21,18 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||||
|
import androidx.compose.material.icons.filled.ChatBubbleOutline
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.Group
|
||||||
|
import androidx.compose.material.icons.filled.Star
|
||||||
|
import androidx.compose.material.icons.filled.StarBorder
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.CheckboxDefaults
|
import androidx.compose.material3.CheckboxDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
@@ -49,6 +58,7 @@ import androidx.compose.ui.graphics.SolidColor
|
|||||||
import androidx.compose.ui.text.PlatformTextStyle
|
import androidx.compose.ui.text.PlatformTextStyle
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.geometry.Rect
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.layout.boundsInWindow
|
import androidx.compose.ui.layout.boundsInWindow
|
||||||
@@ -998,10 +1008,10 @@ fun FilesDetailsSheet(
|
|||||||
loading: Boolean,
|
loading: Boolean,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onTabChange: (FileDetailsTab) -> Unit,
|
onTabChange: (FileDetailsTab) -> Unit,
|
||||||
|
onToggleFavorite: () -> Unit,
|
||||||
onWebOnlyAction: (String) -> Unit,
|
onWebOnlyAction: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
if (item == null) return
|
if (item == null) return
|
||||||
val base = serverUrl.trimEnd('/')
|
|
||||||
var commentDraft by remember(item.relativePath) { mutableStateOf("") }
|
var commentDraft by remember(item.relativePath) { mutableStateOf("") }
|
||||||
androidx.compose.ui.window.Dialog(onDismissRequest = onDismiss) {
|
androidx.compose.ui.window.Dialog(onDismissRequest = onDismiss) {
|
||||||
Surface(
|
Surface(
|
||||||
@@ -1016,9 +1026,26 @@ fun FilesDetailsSheet(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(20.dp),
|
.padding(20.dp),
|
||||||
) {
|
) {
|
||||||
|
// Крестик закрытия — правый верхний угол (по макету)
|
||||||
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable(onClick = onDismiss),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Close,
|
||||||
|
contentDescription = "Закрыть",
|
||||||
|
tint = F7Colors.TextSecondary,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
verticalAlignment = Alignment.Top,
|
verticalAlignment = Alignment.Top,
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
@@ -1029,52 +1056,86 @@ fun FilesDetailsSheet(
|
|||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(6.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
val metaParts = buildList {
|
// Чип владельца (аватар-инициал + логин) — по макету
|
||||||
item.size?.let { add(formatFileSize(it)) }
|
Row(
|
||||||
item.lastModified?.let {
|
modifier = Modifier
|
||||||
add(
|
.clip(RoundedCornerShape(100.dp))
|
||||||
SimpleDateFormat("d MMMM", Locale("ru")).format(Date(it)),
|
.background(F7Colors.SurfaceMuted)
|
||||||
|
.padding(start = 4.dp, end = 10.dp, top = 4.dp, bottom = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(20.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(F7Colors.PrimaryLight),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
ownerLabel.firstOrNull()?.uppercaseChar()?.toString() ?: "?",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = F7Colors.Primary,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
add("Владелец $ownerLabel")
|
|
||||||
}
|
|
||||||
Text(
|
Text(
|
||||||
metaParts.joinToString(" • "),
|
ownerLabel,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = F7Colors.TextSecondary,
|
color = F7Colors.TextSecondary,
|
||||||
maxLines = 2,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
TextButton(onClick = onDismiss) {
|
}
|
||||||
Text("✕", style = MaterialTheme.typography.titleMedium)
|
Column(horizontalAlignment = Alignment.End) {
|
||||||
|
val dateSize = buildList {
|
||||||
|
item.lastModified?.let {
|
||||||
|
add(SimpleDateFormat("d MMMM", Locale("ru")).format(Date(it)))
|
||||||
|
}
|
||||||
|
item.size?.let { add(formatFileSize(it)) }
|
||||||
|
}
|
||||||
|
if (dateSize.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
dateSize.joinToString(" / "),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = F7Colors.TextSecondary,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
if (item.favorite) Icons.Filled.Star else Icons.Filled.StarBorder,
|
||||||
|
contentDescription = "Избранное",
|
||||||
|
tint = if (item.favorite) F7Colors.Primary else F7Colors.TextSecondary,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable(onClick = onToggleFavorite),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
FilesDetailsTabButton(
|
FilesDetailsTabPill(
|
||||||
serverUrl = serverUrl,
|
icon = Icons.Filled.ChatBubbleOutline,
|
||||||
label = "Общий доступ",
|
contentDescription = "Обсуждение",
|
||||||
iconPath = "files/many-person-gray.svg",
|
|
||||||
selected = tab == FileDetailsTab.Sharing,
|
|
||||||
onClick = { onTabChange(FileDetailsTab.Sharing) },
|
|
||||||
)
|
|
||||||
FilesDetailsTabButton(
|
|
||||||
serverUrl = serverUrl,
|
|
||||||
label = "События",
|
|
||||||
iconPath = "files/grid-files-gray.svg",
|
|
||||||
selected = tab == FileDetailsTab.Events,
|
selected = tab == FileDetailsTab.Events,
|
||||||
onClick = { onTabChange(FileDetailsTab.Events) },
|
onClick = { onTabChange(FileDetailsTab.Events) },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
FilesDetailsTabPill(
|
||||||
|
icon = Icons.Filled.Group,
|
||||||
|
contentDescription = "Общий доступ",
|
||||||
|
selected = tab == FileDetailsTab.Sharing,
|
||||||
|
onClick = { onTabChange(FileDetailsTab.Sharing) },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
HorizontalDivider(
|
Spacer(Modifier.height(16.dp))
|
||||||
modifier = Modifier.padding(top = 8.dp),
|
|
||||||
color = F7Colors.Border.copy(alpha = 0.5f),
|
|
||||||
)
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
@@ -1117,42 +1178,28 @@ fun FilesDetailsSheet(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun FilesDetailsTabButton(
|
private fun FilesDetailsTabPill(
|
||||||
serverUrl: String,
|
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||||
label: String,
|
contentDescription: String,
|
||||||
iconPath: String,
|
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val base = serverUrl.trimEnd('/')
|
val shape = RoundedCornerShape(14.dp)
|
||||||
Column(
|
|
||||||
modifier = Modifier.clickable(onClick = onClick),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
|
||||||
) {
|
|
||||||
AsyncImage(
|
|
||||||
model = "$base/themes/forbion/images/$iconPath",
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(16.dp),
|
|
||||||
contentScale = ContentScale.Fit,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Medium,
|
|
||||||
color = if (selected) F7Colors.TextPrimary else F7Colors.TextSecondary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.height(2.dp)
|
.height(40.dp)
|
||||||
.width(72.dp)
|
.clip(shape)
|
||||||
.clip(RoundedCornerShape(1.dp))
|
.background(if (selected) F7Colors.PrimaryLight else F7Colors.Surface)
|
||||||
.background(if (selected) F7Colors.Primary else Color.Transparent),
|
.border(1.dp, if (selected) Color.Transparent else F7Colors.Border, shape)
|
||||||
|
.clickable(onClick = onClick),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
tint = if (selected) F7Colors.Primary else F7Colors.TextSecondary,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1348,8 +1395,24 @@ private fun FilesEventsTabContent(
|
|||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
onEnter = onSubmitComment,
|
onEnter = onSubmitComment,
|
||||||
)
|
)
|
||||||
TextButton(onClick = onSubmitComment) {
|
Box(
|
||||||
Text("→", fontSize = 20.sp)
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
Brush.linearGradient(
|
||||||
|
listOf(F7Colors.PrimaryGradientStart, F7Colors.PrimaryGradientEnd),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.clickable(onClick = onSubmitComment),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.AutoMirrored.Filled.ArrowForward,
|
||||||
|
contentDescription = "Отправить",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
|||||||
import ru.forbion.f7cloud.core.auth.AuthSession
|
import ru.forbion.f7cloud.core.auth.AuthSession
|
||||||
import ru.forbion.f7cloud.core.designsystem.F7AlertDialog
|
import ru.forbion.f7cloud.core.designsystem.F7AlertDialog
|
||||||
import ru.forbion.f7cloud.core.designsystem.F7Colors
|
import ru.forbion.f7cloud.core.designsystem.F7Colors
|
||||||
|
import ru.forbion.f7cloud.core.designsystem.F7CreateButton
|
||||||
import ru.forbion.f7cloud.core.designsystem.F7ModuleScreen
|
import ru.forbion.f7cloud.core.designsystem.F7ModuleScreen
|
||||||
import ru.forbion.f7cloud.core.designsystem.F7OverlayDismissHandler
|
import ru.forbion.f7cloud.core.designsystem.F7OverlayDismissHandler
|
||||||
import ru.forbion.f7cloud.core.designsystem.F7OutlinedField
|
import ru.forbion.f7cloud.core.designsystem.F7OutlinedField
|
||||||
@@ -189,6 +190,9 @@ fun FilesScreen(
|
|||||||
FilesViewToggleButton(viewMode) {
|
FilesViewToggleButton(viewMode) {
|
||||||
viewMode = if (viewMode == FilesViewMode.List) FilesViewMode.Grid else FilesViewMode.List
|
viewMode = if (viewMode == FilesViewMode.List) FilesViewMode.Grid else FilesViewMode.List
|
||||||
}
|
}
|
||||||
|
// Создание (загрузка/новая папка) — раньше вызывалось из нижней панели,
|
||||||
|
// теперь кнопка в шапке (нижняя панель стала навигационной)
|
||||||
|
F7CreateButton(onClick = { showCreateMenu = true }, size = 40.dp)
|
||||||
}
|
}
|
||||||
// Хлебные крошки
|
// Хлебные крошки
|
||||||
val crumbSegments = state.currentPath.split("/").filter { it.isNotBlank() }
|
val crumbSegments = state.currentPath.split("/").filter { it.isNotBlank() }
|
||||||
@@ -360,6 +364,7 @@ fun FilesScreen(
|
|||||||
loading = state.detailsLoading,
|
loading = state.detailsLoading,
|
||||||
onDismiss = vm::hideDetails,
|
onDismiss = vm::hideDetails,
|
||||||
onTabChange = vm::setDetailsTab,
|
onTabChange = vm::setDetailsTab,
|
||||||
|
onToggleFavorite = { state.detailsItem?.let { vm.toggleFavorite(session, it) } },
|
||||||
onWebOnlyAction = vm::showWebOnlyMessage,
|
onWebOnlyAction = vm::showWebOnlyMessage,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -710,10 +710,11 @@ private fun tagChipColors(tag: MailTag): TagChipColors {
|
|||||||
text = parsed.copy(alpha = 0.95f),
|
text = parsed.copy(alpha = 0.95f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// Точные цвета чипов из макета «Почта · Список сообщений» (Figma, сняты пипеткой)
|
||||||
return when (tag.displayName.lowercase(Locale("ru"))) {
|
return when (tag.displayName.lowercase(Locale("ru"))) {
|
||||||
"работа", "work" -> TagChipColors(Color(0xFFE8F5D6), Color(0xFF5A7A2E))
|
"работа", "work" -> TagChipColors(Color(0xFFDAEDD6), Color(0xFF28A413))
|
||||||
"позже", "later" -> TagChipColors(Color(0xFFF5F0E6), Color(0xFF8A7345))
|
"позже", "later" -> TagChipColors(Color(0xFFF1EEE0), Color(0xFFC1B464))
|
||||||
"личное", "personal" -> TagChipColors(Color(0xFFEDE8F5), Color(0xFF6A558A))
|
"личное", "personal" -> TagChipColors(Color(0xFFEFE4FB), Color(0xFFB574F7))
|
||||||
else -> TagChipColors(F7Colors.SurfaceMuted, F7Colors.TextSecondary)
|
else -> TagChipColors(F7Colors.SurfaceMuted, F7Colors.TextSecondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,14 @@ fun MailMessageBodyView(
|
|||||||
settings.domStorageEnabled = false
|
settings.domStorageEnabled = false
|
||||||
settings.loadsImagesAutomatically = true
|
settings.loadsImagesAutomatically = true
|
||||||
settings.mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
|
settings.mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
|
||||||
|
// Вписывать письмо по ширине экрана: уважать viewport width=device-width
|
||||||
|
// и масштабировать широкие письма (таблицы Битрикс/рассылок) в обзорный вид,
|
||||||
|
// иначе WebView раскладывает по ~980px и контент уезжает за правый край.
|
||||||
|
settings.useWideViewPort = true
|
||||||
|
settings.loadWithOverviewMode = true
|
||||||
|
settings.setSupportZoom(true)
|
||||||
|
settings.builtInZoomControls = true
|
||||||
|
settings.displayZoomControls = false
|
||||||
isVerticalScrollBarEnabled = true
|
isVerticalScrollBarEnabled = true
|
||||||
isNestedScrollingEnabled = true
|
isNestedScrollingEnabled = true
|
||||||
setBackgroundColor(0x00000000)
|
setBackgroundColor(0x00000000)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package ru.forbion.f7cloud.feature.talk
|
package ru.forbion.f7cloud.feature.talk
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
@@ -47,6 +48,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import coil.compose.SubcomposeAsyncImage
|
import coil.compose.SubcomposeAsyncImage
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import okhttp3.Credentials
|
import okhttp3.Credentials
|
||||||
@@ -87,12 +89,13 @@ fun TalkChatBackground(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(F7Colors.ChatBackground),
|
.background(F7Colors.ChatBackground),
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
// Локальная подложка чата из дизайна (Подложка2): светло-зелёный градиент + водяной
|
||||||
model = TalkAssets.spreed(session, "chat-pattern.svg"),
|
// паттерн иконок. Была с сервера — теперь офлайн и точно из макета.
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.chat_background),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
alpha = 0.35f,
|
|
||||||
)
|
)
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -304,7 +304,6 @@ fun TalkScreen(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
loading = state.loading && state.rooms.isEmpty(),
|
loading = state.loading && state.rooms.isEmpty(),
|
||||||
error = state.error,
|
error = state.error,
|
||||||
onRefresh = { vm.load(session) },
|
|
||||||
) {
|
) {
|
||||||
RoomListContent(
|
RoomListContent(
|
||||||
session = session,
|
session = session,
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
Reference in New Issue
Block a user