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,