From 6895f1bffd5573a0d20682ee49f9d1345c496a67 Mon Sep 17 00:00:00 2001 From: b-dev-mobile Date: Thu, 9 Jul 2026 20:07:59 +0000 Subject: [PATCH] =?UTF-8?q?design(files):=20=D0=B3=D0=BB=D0=B0=D0=B2=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=201:1=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=B6=D0=B8=D0=B2=D0=BE=D0=B9=20=D1=82=D0=B5=D0=BC?= =?UTF-8?q?=D0=B5=20forbion=20(=5Fmobile-files.css)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Источник — мобильный CSS боевого сервера (копия в design-reference). - 9 фирменных SVG хрома забандлены в assets/chrome/ (офлайн): чекбоксы 16dp (контур/зелёный), звезда избранного, ⋮, лупа, дом, тумблеры вида, цветная папка (закрыт TODO «папка с сервера»). - Строка списка: высота 44dp, чекбокс темы 16 в колонке 44, иконка 30 (было 24) + звезда-оверлей у избранных, колонки размера (56, центр) и даты (88, вправо, относительное время как в вебе), ⋮ 16 в колонке 32, без скругления, выбранная — #ECF9DE. - Тумблер вида: круг 40, иконка 16 из темы. Крошки: дом 16 (home-bottom), все сегменты серые 14/20, шеврон 16. Фильтр-чипы: пилюли 25dp, minWidth 86, текст чёрный, фон #FBFBFB + мягкая тень (как в теме). - Карточка сетки: радиус 12, иконка 80 (было 48), чекбокс/звезда 16 по углам, ⋮ 16, фон #FBFBFB. - Поиск: фон #FBFBFB, локальная лупа из CSS темы, крестик-очистка 20dp. Co-Authored-By: Claude Fable 5 --- .../chrome/checkbox-blank-outline-icon.svg | 15 ++ .../assets/chrome/checkbox-marked-icon.svg | 16 ++ .../main/assets/chrome/dots-vertic-gray.svg | 12 ++ .../assets/chrome/folder-icon-colored.svg | 17 ++ .../main/assets/chrome/grid-files-gray.svg | 15 ++ .../src/main/assets/chrome/home-bottom.svg | 21 +++ .../src/main/assets/chrome/list-view-icon.svg | 15 ++ .../src/main/assets/chrome/search-gray.svg | 1 + .../main/assets/chrome/star-green-full.svg | 10 ++ .../f7cloud/feature/files/FileIcons.kt | 7 +- .../feature/files/FilesBrowserChrome.kt | 147 +++++++++++------- .../f7cloud/feature/files/FilesComponents.kt | 94 ++++++++--- 12 files changed, 290 insertions(+), 80 deletions(-) create mode 100644 feature/files/src/main/assets/chrome/checkbox-blank-outline-icon.svg create mode 100644 feature/files/src/main/assets/chrome/checkbox-marked-icon.svg create mode 100644 feature/files/src/main/assets/chrome/dots-vertic-gray.svg create mode 100644 feature/files/src/main/assets/chrome/folder-icon-colored.svg create mode 100644 feature/files/src/main/assets/chrome/grid-files-gray.svg create mode 100644 feature/files/src/main/assets/chrome/home-bottom.svg create mode 100644 feature/files/src/main/assets/chrome/list-view-icon.svg create mode 100644 feature/files/src/main/assets/chrome/search-gray.svg create mode 100644 feature/files/src/main/assets/chrome/star-green-full.svg diff --git a/feature/files/src/main/assets/chrome/checkbox-blank-outline-icon.svg b/feature/files/src/main/assets/chrome/checkbox-blank-outline-icon.svg new file mode 100644 index 0000000..48974a9 --- /dev/null +++ b/feature/files/src/main/assets/chrome/checkbox-blank-outline-icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/feature/files/src/main/assets/chrome/checkbox-marked-icon.svg b/feature/files/src/main/assets/chrome/checkbox-marked-icon.svg new file mode 100644 index 0000000..aa24a07 --- /dev/null +++ b/feature/files/src/main/assets/chrome/checkbox-marked-icon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/feature/files/src/main/assets/chrome/dots-vertic-gray.svg b/feature/files/src/main/assets/chrome/dots-vertic-gray.svg new file mode 100644 index 0000000..55c43c5 --- /dev/null +++ b/feature/files/src/main/assets/chrome/dots-vertic-gray.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/feature/files/src/main/assets/chrome/folder-icon-colored.svg b/feature/files/src/main/assets/chrome/folder-icon-colored.svg new file mode 100644 index 0000000..f43e9da --- /dev/null +++ b/feature/files/src/main/assets/chrome/folder-icon-colored.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/feature/files/src/main/assets/chrome/grid-files-gray.svg b/feature/files/src/main/assets/chrome/grid-files-gray.svg new file mode 100644 index 0000000..0a492b6 --- /dev/null +++ b/feature/files/src/main/assets/chrome/grid-files-gray.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/feature/files/src/main/assets/chrome/home-bottom.svg b/feature/files/src/main/assets/chrome/home-bottom.svg new file mode 100644 index 0000000..7387919 --- /dev/null +++ b/feature/files/src/main/assets/chrome/home-bottom.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/feature/files/src/main/assets/chrome/list-view-icon.svg b/feature/files/src/main/assets/chrome/list-view-icon.svg new file mode 100644 index 0000000..42664cb --- /dev/null +++ b/feature/files/src/main/assets/chrome/list-view-icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/feature/files/src/main/assets/chrome/search-gray.svg b/feature/files/src/main/assets/chrome/search-gray.svg new file mode 100644 index 0000000..31d21dd --- /dev/null +++ b/feature/files/src/main/assets/chrome/search-gray.svg @@ -0,0 +1 @@ + diff --git a/feature/files/src/main/assets/chrome/star-green-full.svg b/feature/files/src/main/assets/chrome/star-green-full.svg new file mode 100644 index 0000000..671e31f --- /dev/null +++ b/feature/files/src/main/assets/chrome/star-green-full.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/feature/files/src/main/java/ru/forbion/f7cloud/feature/files/FileIcons.kt b/feature/files/src/main/java/ru/forbion/f7cloud/feature/files/FileIcons.kt index c2285b8..9798fb7 100644 --- a/feature/files/src/main/java/ru/forbion/f7cloud/feature/files/FileIcons.kt +++ b/feature/files/src/main/java/ru/forbion/f7cloud/feature/files/FileIcons.kt @@ -1,13 +1,16 @@ package ru.forbion.f7cloud.feature.files object FileIcons { + /** Локальные SVG хрома Файлов из боевой темы forbion (assets/chrome/, coil-svg). */ + fun chromeAsset(name: String): String = "file:///android_asset/chrome/$name" + /** * Иконка типа файла. Типы файлов — ЛОКАЛЬНЫЕ SVG из дизайна (assets/filetypes/, грузятся * coil-svg): офлайн + точные иконки макета (раньше тянулись с сервера — аудит: офлайн ломался). - * Папка — пока с сервера (иконки папки в наборе дизайна нет; TODO забрать из Figma). + * Папка — фирменная folder-icon-colored.svg из боевой темы (тоже локально). */ fun iconUrl(serverUrl: String, name: String, isDirectory: Boolean): String { - if (isDirectory) return "${serverUrl.trimEnd('/')}/core/img/filetypes/folder.svg" + if (isDirectory) return chromeAsset("folder-icon-colored.svg") return "file:///android_asset/filetypes/${localIcon(name)}.svg" } diff --git a/feature/files/src/main/java/ru/forbion/f7cloud/feature/files/FilesBrowserChrome.kt b/feature/files/src/main/java/ru/forbion/f7cloud/feature/files/FilesBrowserChrome.kt index e32a872..81c65a5 100644 --- a/feature/files/src/main/java/ru/forbion/f7cloud/feature/files/FilesBrowserChrome.kt +++ b/feature/files/src/main/java/ru/forbion/f7cloud/feature/files/FilesBrowserChrome.kt @@ -4,34 +4,32 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.List -import androidx.compose.material.icons.filled.GridView -import androidx.compose.material.icons.filled.Home -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.filled.Star -import androidx.compose.material.icons.filled.StarBorder -import androidx.compose.material3.Checkbox -import androidx.compose.material3.CheckboxDefaults +import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.draw.shadow import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -40,30 +38,59 @@ import ru.forbion.f7cloud.core.designsystem.F7Colors import ru.forbion.f7cloud.core.designsystem.F7Radius import ru.forbion.f7cloud.core.designsystem.F7Spacing +// Размеры — из боевой темы forbion (_mobile-files.css), копия один в один. + enum class FilesViewMode { List, Grid } -/** Квадратная кнопка-переключатель вида список⇄сетка справа от поиска (макет F-17/F-22). */ +/** + * Чекбокс из боевой темы: 16×16, серый контур / зелёная заливка с галкой + * (checkbox-blank-outline-icon.svg / checkbox-marked-icon.svg). Тап-зона задаётся снаружи. + */ +@Composable +fun F7ThemeCheckbox(checked: Boolean, onToggle: () -> Unit, modifier: Modifier = Modifier) { + AsyncImage( + model = FileIcons.chromeAsset( + if (checked) "checkbox-marked-icon.svg" else "checkbox-blank-outline-icon.svg", + ), + contentDescription = if (checked) "Снять выбор" else "Выбрать", + modifier = modifier + .size(16.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onToggle, + ), + ) +} + +/** Стиль «пилюли» темы: фон #FBFBFB, рамка #E6E6E6, мягкая тень 0 1px 2px. */ +private fun Modifier.f7PillSurface(shape: RoundedCornerShape): Modifier = this + .shadow(1.dp, shape, ambientColor = F7Colors.Border, spotColor = F7Colors.Border) + .clip(shape) + .background(F7Colors.Background) + .border(1.dp, F7Colors.Border, shape) + +/** Круглая кнопка-переключатель список⇄сетка справа от поиска (40×40, иконка 16). */ @Composable fun FilesViewToggleButton(viewMode: FilesViewMode, onToggle: () -> Unit) { Box( modifier = Modifier .size(40.dp) - .clip(RoundedCornerShape(F7Radius.md)) - .background(F7Colors.SecondaryButtonBg) - .border(1.dp, F7Colors.Border, RoundedCornerShape(F7Radius.md)) + .f7PillSurface(RoundedCornerShape(F7Radius.pill)) .clickable(onClick = onToggle), contentAlignment = Alignment.Center, ) { - Icon( - imageVector = if (viewMode == FilesViewMode.List) Icons.Filled.GridView else Icons.AutoMirrored.Filled.List, + AsyncImage( + model = FileIcons.chromeAsset( + if (viewMode == FilesViewMode.List) "grid-files-gray.svg" else "list-view-icon.svg", + ), contentDescription = "Сменить вид", - tint = F7Colors.TextSecondary, - modifier = Modifier.size(20.dp), + modifier = Modifier.size(16.dp), ) } } -/** Хлебные крошки: домой + сегменты пути через ›, прокрутка по горизонтали (макет F-21/F-22). */ +/** Хлебные крошки: дом 16 + сегменты (все — серые 14/20) через шеврон 16, h-прокрутка. */ @Composable fun FilesBreadcrumb( segments: List, @@ -75,23 +102,28 @@ fun FilesBreadcrumb( modifier = modifier .fillMaxWidth() .horizontalScroll(rememberScrollState()) - .padding(vertical = F7Spacing.sm), + .padding(horizontal = F7Spacing.sm, vertical = F7Spacing.xs), + horizontalArrangement = Arrangement.spacedBy(F7Spacing.xs), verticalAlignment = Alignment.CenterVertically, ) { - Icon( - imageVector = Icons.Filled.Home, + AsyncImage( + model = FileIcons.chromeAsset("home-bottom.svg"), contentDescription = "В начало", - tint = F7Colors.TextSecondary, modifier = Modifier - .size(22.dp) + .size(16.dp) .clickable(onClick = onHomeClick), ) segments.forEachIndexed { i, seg -> - Text(" › ", style = MaterialTheme.typography.bodyMedium, color = F7Colors.TextSecondary) + Icon( + imageVector = Icons.Filled.ChevronRight, + contentDescription = null, + tint = F7Colors.TextSecondary, + modifier = Modifier.size(16.dp), + ) Text( text = seg, style = MaterialTheme.typography.bodyMedium, - color = if (i == segments.lastIndex) F7Colors.TextPrimary else F7Colors.TextSecondary, + color = F7Colors.TextSecondary, maxLines = 1, modifier = Modifier.clickable { onSegmentClick(i) }, ) @@ -99,7 +131,7 @@ fun FilesBreadcrumb( } } -/** Ряд фильтр-чипов: Тип файла / Участники / Изменен (макет F-18/F-22). */ +/** Ряд фильтр-чипов: Тип файла / Участники / Изменен (пилюли 25dp, minWidth 86). */ @Composable fun FilesFilterChips( onTypeClick: () -> Unit, @@ -111,7 +143,7 @@ fun FilesFilterChips( modifier = modifier .fillMaxWidth() .horizontalScroll(rememberScrollState()) - .padding(vertical = F7Spacing.xs), + .padding(horizontal = F7Spacing.xs, vertical = F7Spacing.xs), horizontalArrangement = Arrangement.spacedBy(F7Spacing.sm), verticalAlignment = Alignment.CenterVertically, ) { @@ -125,16 +157,21 @@ fun FilesFilterChips( private fun FilterChip(label: String, onClick: () -> Unit) { Box( modifier = Modifier - .clip(RoundedCornerShape(F7Radius.pill)) - .border(1.dp, F7Colors.Border, RoundedCornerShape(F7Radius.pill)) + .height(25.dp) + .widthIn(min = 86.dp) + .f7PillSurface(RoundedCornerShape(F7Radius.pill)) .clickable(onClick = onClick) - .padding(horizontal = F7Spacing.md, vertical = F7Spacing.xs), + .padding(horizontal = F7Spacing.sm), + contentAlignment = Alignment.Center, ) { - Text(label, style = MaterialTheme.typography.bodyMedium, color = F7Colors.TextSecondary) + Text(label, style = MaterialTheme.typography.bodyMedium, color = F7Colors.TextPrimary) } } -/** Карточка сетки: чекбокс + звезда сверху, крупная иконка по центру, имя + ⋮ снизу (макет F-17). */ +/** + * Карточка сетки (2 колонки): чекбокс 16 слева-сверху, зелёная звезда 16 справа-сверху + * (только у избранных), иконка 80 по центру, имя 14/20 + ⋮ 16 снизу. Радиус 12, отступ 12. + */ @Composable fun FilesGridCard( serverUrl: String, @@ -146,41 +183,40 @@ fun FilesGridCard( onMenuClick: () -> Unit, modifier: Modifier = Modifier, ) { + val shape = RoundedCornerShape(12.dp) Column( modifier = modifier - .clip(RoundedCornerShape(F7Radius.lg)) - .background(if (selected) F7Colors.PrimaryLight else F7Colors.Surface) - .border(1.dp, F7Colors.Border, RoundedCornerShape(F7Radius.lg)) + .clip(shape) + .background(if (selected) F7Colors.PrimaryLight else F7Colors.Background) + .border(1.dp, F7Colors.Border, shape) .clickable(onClick = onOpenClick) - .padding(F7Spacing.md), + .padding(12.dp), ) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { - Checkbox( - checked = selected, - onCheckedChange = { onSelectedChange() }, - colors = CheckboxDefaults.colors(checkedColor = F7Colors.Primary), - ) - Spacer(Modifier.width(F7Spacing.sm)) + F7ThemeCheckbox(checked = selected, onToggle = onSelectedChange) Box(modifier = Modifier.weight(1f)) - Icon( - imageVector = if (favorite) Icons.Filled.Star else Icons.Filled.StarBorder, - contentDescription = null, - tint = if (favorite) F7Colors.Primary else F7Colors.TextMuted, - modifier = Modifier.size(18.dp), - ) + if (favorite) { + AsyncImage( + model = FileIcons.chromeAsset("star-green-full.svg"), + contentDescription = "В избранном", + modifier = Modifier.size(16.dp), + ) + } } + Spacer(Modifier.height(F7Spacing.sm)) Box( modifier = Modifier .fillMaxWidth() - .padding(vertical = F7Spacing.lg), + .height(80.dp), contentAlignment = Alignment.Center, ) { AsyncImage( model = FileIcons.iconUrl(serverUrl, item.name, item.isDirectory), contentDescription = null, - modifier = Modifier.size(48.dp), + modifier = Modifier.size(80.dp), ) } + Spacer(Modifier.height(F7Spacing.sm)) Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { Text( text = item.name, @@ -191,12 +227,11 @@ fun FilesGridCard( maxLines = 1, overflow = TextOverflow.Ellipsis, ) - Icon( - imageVector = Icons.Filled.MoreVert, + AsyncImage( + model = FileIcons.chromeAsset("dots-vertic-gray.svg"), contentDescription = "Меню", - tint = F7Colors.TextSecondary, modifier = Modifier - .size(18.dp) + .size(16.dp) .clickable(onClick = onMenuClick), ) } @@ -221,7 +256,7 @@ fun FilesEmptyState( Box( modifier = Modifier .size(72.dp) - .clip(RoundedCornerShape(F7Radius.pill)) + .clip(CircleShape) .background(F7Colors.SurfaceMuted), contentAlignment = Alignment.Center, ) { diff --git a/feature/files/src/main/java/ru/forbion/f7cloud/feature/files/FilesComponents.kt b/feature/files/src/main/java/ru/forbion/f7cloud/feature/files/FilesComponents.kt index 9218ca9..345cf97 100644 --- a/feature/files/src/main/java/ru/forbion/f7cloud/feature/files/FilesComponents.kt +++ b/feature/files/src/main/java/ru/forbion/f7cloud/feature/files/FilesComponents.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -66,6 +67,7 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp @@ -559,7 +561,6 @@ fun FilesSearchBar( onQueryChange: (String) -> Unit, modifier: Modifier = Modifier, ) { - val base = serverUrl.trimEnd('/') Box( modifier = modifier .height(40.dp) @@ -569,7 +570,7 @@ fun FilesSearchBar( spotColor = Color(0xFFCBCBCB), ) .clip(RoundedCornerShape(100.dp)) - .background(Color(0xFFFDFDFD)) + .background(F7Colors.Background) .border(1.dp, F7Colors.Border, RoundedCornerShape(100.dp)), contentAlignment = Alignment.CenterStart, ) { @@ -581,7 +582,7 @@ fun FilesSearchBar( verticalAlignment = Alignment.CenterVertically, ) { AsyncImage( - model = "$base/themes/forbion/images/header/search-gray.svg", + model = FileIcons.chromeAsset("search-gray.svg"), contentDescription = null, modifier = Modifier.size(16.dp), contentScale = ContentScale.Fit, @@ -620,6 +621,16 @@ fun FilesSearchBar( } }, ) + if (query.isNotEmpty()) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = "Очистить поиск", + tint = F7Colors.TextSecondary, + modifier = Modifier + .size(20.dp) + .clickable { onQueryChange("") }, + ) + } } } } @@ -793,27 +804,36 @@ fun FilesListRow( val hasExt = !item.isDirectory && item.name.contains('.') val baseName = if (hasExt) item.name.substringBeforeLast('.') else item.name val extension = if (hasExt && showExtensions) "." + item.name.substringAfterLast('.') else "" + // Строка 44dp по боевой теме (_mobile-files.css): чекбокс 16 в колонке 44, иконка 30 + // (зелёная звезда 16 поверх у избранных), имя, размер 56 по центру, дата 88 вправо, ⋮ в 32. Row( modifier = Modifier .fillMaxWidth() - .clip(RoundedCornerShape(F7Radius.md)) - // выбранная строка — светло-зелёный фон (макет F-21) + .height(44.dp) .background(if (selected) F7Colors.PrimaryLight else Color.Transparent) - .clickable(onClick = onOpenClick) - .padding(horizontal = F7Spacing.sm, vertical = 10.dp), + .clickable(onClick = onOpenClick), verticalAlignment = Alignment.CenterVertically, ) { - Checkbox( - checked = selected, - onCheckedChange = { onSelectedChange() }, - colors = CheckboxDefaults.colors(checkedColor = F7Colors.Primary), - ) - AsyncImage( - model = FileIcons.iconUrl(serverUrl, item.name, item.isDirectory), - contentDescription = null, - modifier = Modifier.size(24.dp), - contentScale = ContentScale.Fit, - ) + Box(modifier = Modifier.size(44.dp), contentAlignment = Alignment.Center) { + F7ThemeCheckbox(checked = selected, onToggle = onSelectedChange) + } + Box(modifier = Modifier.size(30.dp)) { + AsyncImage( + model = FileIcons.iconUrl(serverUrl, item.name, item.isDirectory), + contentDescription = null, + modifier = Modifier.size(30.dp), + contentScale = ContentScale.Fit, + ) + if (item.favorite) { + AsyncImage( + model = FileIcons.chromeAsset("star-green-full.svg"), + contentDescription = "В избранном", + modifier = Modifier + .size(16.dp) + .offset(x = 20.dp, y = (-4).dp), + ) + } + } Text( text = buildAnnotatedString { append(baseName) @@ -823,17 +843,38 @@ fun FilesListRow( }, modifier = Modifier .weight(1f) - .padding(horizontal = F7Spacing.sm), + .padding(start = F7Spacing.sm, end = F7Spacing.xs), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium, color = F7Colors.TextPrimary, maxLines = 1, overflow = TextOverflow.Ellipsis, ) + item.size?.takeIf { !item.isDirectory }?.let { size -> + Text( + text = formatFileSize(size), + modifier = Modifier.width(56.dp), + style = MaterialTheme.typography.bodyMedium, + color = F7Colors.TextPrimary, + maxLines = 1, + textAlign = TextAlign.Center, + ) + } + item.lastModified?.let { mtime -> + Text( + text = formatRelativeTime(mtime), + modifier = Modifier.width(88.dp), + style = MaterialTheme.typography.bodyMedium, + color = F7Colors.TextPrimary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.End, + ) + } Box( modifier = Modifier - .size(36.dp) - .clip(RoundedCornerShape(8.dp)) + .width(32.dp) + .height(44.dp) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, @@ -845,7 +886,7 @@ fun FilesListRow( contentAlignment = Alignment.Center, ) { AsyncImage( - model = "${serverUrl.trimEnd('/')}/themes/forbion/images/files/dots-vertic-gray.svg", + model = FileIcons.chromeAsset("dots-vertic-gray.svg"), contentDescription = "Действия", modifier = Modifier.size(16.dp), contentScale = ContentScale.Fit, @@ -854,6 +895,15 @@ fun FilesListRow( } } +/** Относительное время как в вебе (moment.js): «5 мин. назад», «вчера», иначе дата. */ +internal fun formatRelativeTime(epochMillis: Long): String = + android.text.format.DateUtils.getRelativeTimeSpanString( + epochMillis, + System.currentTimeMillis(), + android.text.format.DateUtils.MINUTE_IN_MILLIS, + android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE, + ).toString() + @Composable fun FileActionMenuPopup( expanded: Boolean,