Дизайн Файлов (чанк 3): рескин каркаса главного экрана под макет
- FilesBrowserChrome.kt: поиск-пилюля + переключатель список⇄сетка, хлебные крошки (навигация navigateToPath), фильтр-чипы (Тип файла/Участники/Изменен) вместо десктопной колоночной шапки, вид СЕТКА (LazyVerticalGrid 2 кол., карточки), пустое состояние (иконка+заголовок+подзаголовок). - Строки списка без разделителей (по макету). Токены F7Spacing/F7Radius. - material-icons-extended → feature:files (иконки chrome; точные из дизайна — потом). feature:files компилируется. Логика (загрузка/выбор/открытие) не менялась.
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
Формат: `ГГГГ-ММ-ДД | версия | изменение | контракты | риск`
|
Формат: `ГГГГ-ММ-ДД | версия | изменение | контракты | риск`
|
||||||
|
|
||||||
|
- 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) | Дизайн Файлов (чанк 2): локальные иконки типов файлов. 13 SVG из дизайна (PDF/Word/Spreadsheet/Presentation/Image/Audio/Video/Archive/Code/Text/Certificate/Eml/file) в assets/filetypes/, FileIcons переключён на них (file:///android_asset, coil-svg). Раньше иконки типов тянулись с сервера → офлайн ломался (аудит) + не гарантированно совпадали с дизайном. Папка пока с сервера (иконки папки в наборе нет — TODO из Figma). Дизайн-макеты (109 PNG) — на инфре B /home/foradmin/figma/, в git не бандлю (вес) | контракты не менялись | низкий: файловые иконки теперь офлайн и точно из дизайна
|
- 2026-07-08 | v0.5.114 (122) | Дизайн Файлов (чанк 2): локальные иконки типов файлов. 13 SVG из дизайна (PDF/Word/Spreadsheet/Presentation/Image/Audio/Video/Archive/Code/Text/Certificate/Eml/file) в assets/filetypes/, FileIcons переключён на них (file:///android_asset, coil-svg). Раньше иконки типов тянулись с сервера → офлайн ломался (аудит) + не гарантированно совпадали с дизайном. Папка пока с сервера (иконки папки в наборе нет — TODO из Figma). Дизайн-макеты (109 PNG) — на инфре B /home/foradmin/figma/, в git не бандлю (вес) | контракты не менялись | низкий: файловые иконки теперь офлайн и точно из дизайна
|
||||||
|
|
||||||
- 2026-07-08 | v0.5.114 (122) | Трек дизайна (старт): снятие дизайн-токенов из Figma роли design (файл ocVUCYCrfFUoqYYvlcLPBF, доступ по API-токену вне git). Цвета сверены — совпадают с F7Colors; типографика — Raleway 14/18/20/24; заведены F7Spacing (4dp-сетка) + F7Radius (аудит: не было spacing-токенов, ~1087 инлайновых .dp). docs/DESIGN-SOURCE.md — карта Figma + процесс попиксельной вёрстки. Источник — Figma, боевой forbion НЕ трогаю. Координация — mail/044 | дизайн-система (аддитивно, пока не используется) | нет: новые токены, поведение не меняется
|
- 2026-07-08 | v0.5.114 (122) | Трек дизайна (старт): снятие дизайн-токенов из Figma роли design (файл ocVUCYCrfFUoqYYvlcLPBF, доступ по API-токену вне git). Цвета сверены — совпадают с F7Colors; типографика — Raleway 14/18/20/24; заведены F7Spacing (4dp-сетка) + F7Radius (аудит: не было spacing-токенов, ~1087 инлайновых .dp). docs/DESIGN-SOURCE.md — карта Figma + процесс попиксельной вёрстки. Источник — Figma, боевой forbion НЕ трогаю. Координация — mail/044 | дизайн-система (аддитивно, пока не используется) | нет: новые токены, поведение не меняется
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ dependencies {
|
|||||||
implementation composeBom
|
implementation composeBom
|
||||||
implementation libs.compose.ui
|
implementation libs.compose.ui
|
||||||
implementation libs.compose.material3
|
implementation libs.compose.material3
|
||||||
|
implementation libs.compose.material.icons.extended
|
||||||
implementation libs.compose.foundation
|
implementation libs.compose.foundation
|
||||||
implementation libs.lifecycle.viewmodel.compose
|
implementation libs.lifecycle.viewmodel.compose
|
||||||
implementation libs.coroutines.android
|
implementation libs.coroutines.android
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
package ru.forbion.f7cloud.feature.files
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
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.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
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.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import ru.forbion.f7cloud.core.designsystem.F7Colors
|
||||||
|
import ru.forbion.f7cloud.core.designsystem.F7Radius
|
||||||
|
import ru.forbion.f7cloud.core.designsystem.F7Spacing
|
||||||
|
|
||||||
|
enum class FilesViewMode { List, Grid }
|
||||||
|
|
||||||
|
/** Квадратная кнопка-переключатель вида список⇄сетка справа от поиска (макет F-17/F-22). */
|
||||||
|
@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))
|
||||||
|
.clickable(onClick = onToggle),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (viewMode == FilesViewMode.List) Icons.Filled.GridView else Icons.AutoMirrored.Filled.List,
|
||||||
|
contentDescription = "Сменить вид",
|
||||||
|
tint = F7Colors.TextSecondary,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Хлебные крошки: домой + сегменты пути через ›, прокрутка по горизонтали (макет F-21/F-22). */
|
||||||
|
@Composable
|
||||||
|
fun FilesBreadcrumb(
|
||||||
|
segments: List<String>,
|
||||||
|
onHomeClick: () -> Unit,
|
||||||
|
onSegmentClick: (index: Int) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.horizontalScroll(rememberScrollState())
|
||||||
|
.padding(vertical = F7Spacing.sm),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Home,
|
||||||
|
contentDescription = "В начало",
|
||||||
|
tint = F7Colors.TextSecondary,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(22.dp)
|
||||||
|
.clickable(onClick = onHomeClick),
|
||||||
|
)
|
||||||
|
segments.forEachIndexed { i, seg ->
|
||||||
|
Text(" › ", style = MaterialTheme.typography.bodyMedium, color = F7Colors.TextSecondary)
|
||||||
|
Text(
|
||||||
|
text = seg,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = if (i == segments.lastIndex) F7Colors.TextPrimary else F7Colors.TextSecondary,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier.clickable { onSegmentClick(i) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ряд фильтр-чипов: Тип файла / Участники / Изменен (макет F-18/F-22). */
|
||||||
|
@Composable
|
||||||
|
fun FilesFilterChips(
|
||||||
|
onTypeClick: () -> Unit,
|
||||||
|
onParticipantsClick: () -> Unit,
|
||||||
|
onModifiedClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.horizontalScroll(rememberScrollState())
|
||||||
|
.padding(vertical = F7Spacing.xs),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(F7Spacing.sm),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
FilterChip("Тип файла", onTypeClick)
|
||||||
|
FilterChip("Участники", onParticipantsClick)
|
||||||
|
FilterChip("Изменен", onModifiedClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FilterChip(label: String, onClick: () -> Unit) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(F7Radius.pill))
|
||||||
|
.border(1.dp, F7Colors.Border, RoundedCornerShape(F7Radius.pill))
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(horizontal = F7Spacing.md, vertical = F7Spacing.xs),
|
||||||
|
) {
|
||||||
|
Text(label, style = MaterialTheme.typography.bodyMedium, color = F7Colors.TextSecondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Карточка сетки: чекбокс + звезда сверху, крупная иконка по центру, имя + ⋮ снизу (макет F-17). */
|
||||||
|
@Composable
|
||||||
|
fun FilesGridCard(
|
||||||
|
serverUrl: String,
|
||||||
|
item: FileItem,
|
||||||
|
selected: Boolean,
|
||||||
|
favorite: Boolean,
|
||||||
|
onSelectedChange: () -> Unit,
|
||||||
|
onOpenClick: () -> Unit,
|
||||||
|
onMenuClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(RoundedCornerShape(F7Radius.lg))
|
||||||
|
.background(if (selected) F7Colors.PrimaryLight else F7Colors.Surface)
|
||||||
|
.border(1.dp, F7Colors.Border, RoundedCornerShape(F7Radius.lg))
|
||||||
|
.clickable(onClick = onOpenClick)
|
||||||
|
.padding(F7Spacing.md),
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Checkbox(
|
||||||
|
checked = selected,
|
||||||
|
onCheckedChange = { onSelectedChange() },
|
||||||
|
colors = CheckboxDefaults.colors(checkedColor = F7Colors.Primary),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(F7Spacing.sm))
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = F7Spacing.lg),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = FileIcons.iconUrl(serverUrl, item.name, item.isDirectory),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
text = item.name,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = F7Colors.TextPrimary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.MoreVert,
|
||||||
|
contentDescription = "Меню",
|
||||||
|
tint = F7Colors.TextSecondary,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(18.dp)
|
||||||
|
.clickable(onClick = onMenuClick),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Пустое состояние: круг-иконка + заголовок + подзаголовок + опц. кнопка (макет F-11..16). */
|
||||||
|
@Composable
|
||||||
|
fun FilesEmptyState(
|
||||||
|
iconUrl: String,
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
action: (@Composable () -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 96.dp, start = F7Spacing.xxl, end = F7Spacing.xxl),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(72.dp)
|
||||||
|
.clip(RoundedCornerShape(F7Radius.pill))
|
||||||
|
.background(F7Colors.SurfaceMuted),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
AsyncImage(model = iconUrl, contentDescription = null, modifier = Modifier.size(32.dp))
|
||||||
|
}
|
||||||
|
Spacer(Modifier.size(F7Spacing.lg))
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = F7Colors.TextPrimary,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.size(F7Spacing.xs))
|
||||||
|
Text(
|
||||||
|
subtitle,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = F7Colors.TextSecondary,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
|
||||||
|
)
|
||||||
|
if (action != null) {
|
||||||
|
Spacer(Modifier.size(F7Spacing.xl))
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -547,12 +547,11 @@ fun FilesSearchBar(
|
|||||||
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(
|
.shadow(
|
||||||
elevation = 2.dp,
|
elevation = 2.dp,
|
||||||
|
|||||||
@@ -8,8 +8,14 @@ import androidx.compose.foundation.layout.Column
|
|||||||
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.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items as gridItems
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
@@ -61,6 +67,7 @@ fun FilesScreen(
|
|||||||
val vm: FilesViewModel = viewModel(factory = FilesViewModel.Factory(context))
|
val vm: FilesViewModel = viewModel(factory = FilesViewModel.Factory(context))
|
||||||
val state by vm.state.collectAsState()
|
val state by vm.state.collectAsState()
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
var viewMode by remember { mutableStateOf(FilesViewMode.List) }
|
||||||
var showCreateMenu by remember { mutableStateOf(false) }
|
var showCreateMenu by remember { mutableStateOf(false) }
|
||||||
var showNewFolderDialog by remember { mutableStateOf(false) }
|
var showNewFolderDialog by remember { mutableStateOf(false) }
|
||||||
var newFolderName by remember { mutableStateOf("") }
|
var newFolderName by remember { mutableStateOf("") }
|
||||||
@@ -167,23 +174,28 @@ fun FilesScreen(
|
|||||||
headerActions = {},
|
headerActions = {},
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
if (state.currentPath.isNotBlank() && state.browseMode == FilesBrowseMode.AllFiles) {
|
// Поиск + переключатель вид список⇄сетка (макет F-17/F-22)
|
||||||
F7SecondaryButton(
|
Row(
|
||||||
text = "Назад",
|
modifier = Modifier.fillMaxWidth(),
|
||||||
onClick = { vm.goUp(session) },
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = Modifier.padding(bottom = 8.dp),
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
)
|
) {
|
||||||
}
|
|
||||||
Text(
|
|
||||||
text = pathLabel,
|
|
||||||
style = MaterialTheme.typography.titleSmall,
|
|
||||||
color = F7Colors.TextPrimary,
|
|
||||||
modifier = Modifier.padding(bottom = 8.dp),
|
|
||||||
)
|
|
||||||
FilesSearchBar(
|
FilesSearchBar(
|
||||||
serverUrl = session.serverUrl,
|
serverUrl = session.serverUrl,
|
||||||
query = state.searchQuery,
|
query = state.searchQuery,
|
||||||
onQueryChange = { vm.setSearchQuery(session, it) },
|
onQueryChange = { vm.setSearchQuery(session, it) },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
FilesViewToggleButton(viewMode) {
|
||||||
|
viewMode = if (viewMode == FilesViewMode.List) FilesViewMode.Grid else FilesViewMode.List
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Хлебные крошки
|
||||||
|
val crumbSegments = state.currentPath.split("/").filter { it.isNotBlank() }
|
||||||
|
FilesBreadcrumb(
|
||||||
|
segments = crumbSegments,
|
||||||
|
onHomeClick = { vm.navigateToPath(session, "") },
|
||||||
|
onSegmentClick = { i -> vm.navigateToPath(session, crumbSegments.take(i + 1).joinToString("/")) },
|
||||||
)
|
)
|
||||||
if (selectionMode) {
|
if (selectionMode) {
|
||||||
FilesBulkActionsBar(
|
FilesBulkActionsBar(
|
||||||
@@ -195,17 +207,11 @@ fun FilesScreen(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
FilesListHeader(
|
// Фильтр-чипы вместо десктопной колоночной шапки (макет F-18/F-22)
|
||||||
serverUrl = session.serverUrl,
|
FilesFilterChips(
|
||||||
showMimeColumn = state.userConfig.showMimeColumn,
|
onTypeClick = { vm.setSortColumn(FilesSortColumn.Type) },
|
||||||
sortColumn = state.sortColumn,
|
onParticipantsClick = {},
|
||||||
sortDirection = state.sortDirection,
|
onModifiedClick = { vm.setSortColumn(FilesSortColumn.Modified) },
|
||||||
selectionMode = false,
|
|
||||||
allSelected = allSelected,
|
|
||||||
onSelectAllToggle = {
|
|
||||||
if (allSelected) vm.clearSelection() else vm.selectAllVisible()
|
|
||||||
},
|
|
||||||
onSortColumnClick = vm::setSortColumn,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (state.openingFile != null) {
|
if (state.openingFile != null) {
|
||||||
@@ -223,43 +229,65 @@ fun FilesScreen(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
) {
|
) {
|
||||||
|
val openItem: (FileItem) -> Unit = { item ->
|
||||||
|
val openable = !item.isDirectory &&
|
||||||
|
(OfficeFiles.isOfficeFile(item.name) || OpenableFiles.isOpenable(item.name))
|
||||||
|
when {
|
||||||
|
item.isDirectory -> vm.openFolder(session, item)
|
||||||
|
openable -> vm.openItem(session, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
if (displayItems.isEmpty() && !state.loading && state.error.isNullOrBlank()) {
|
if (displayItems.isEmpty() && !state.loading && state.error.isNullOrBlank()) {
|
||||||
Text(
|
val searching = state.searchQuery.isNotBlank()
|
||||||
text = if (state.searchQuery.isNotBlank()) "Ничего не найдено" else "Папка пуста",
|
FilesEmptyState(
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
iconUrl = "file:///android_asset/filetypes/file.svg",
|
||||||
modifier = Modifier.padding(top = 8.dp),
|
title = if (searching) "Нет результатов поиска" else "Папка пуста",
|
||||||
|
subtitle = if (searching) {
|
||||||
|
"По запросу «${state.searchQuery}» ничего не найдено"
|
||||||
|
} else {
|
||||||
|
"Загрузите контент или синхронизируйте его своими устройствами"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else if (viewMode == FilesViewMode.Grid) {
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(2),
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
contentPadding = PaddingValues(vertical = 8.dp),
|
||||||
|
) {
|
||||||
|
gridItems(displayItems, key = { it.relativePath }) { item ->
|
||||||
|
FilesGridCard(
|
||||||
|
serverUrl = session.serverUrl,
|
||||||
|
item = item,
|
||||||
|
selected = item.relativePath in state.selectedPaths,
|
||||||
|
favorite = item.favorite,
|
||||||
|
onSelectedChange = { vm.toggleSelection(item.relativePath) },
|
||||||
|
onOpenClick = { openItem(item) },
|
||||||
|
onMenuClick = {
|
||||||
|
contextMenuItem = item
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(displayItems, key = { _, item -> item.relativePath }) { index, item ->
|
items(displayItems, key = { it.relativePath }) { item ->
|
||||||
val openable = !item.isDirectory &&
|
|
||||||
(OfficeFiles.isOfficeFile(item.name) || OpenableFiles.isOpenable(item.name))
|
|
||||||
FilesListRow(
|
FilesListRow(
|
||||||
serverUrl = session.serverUrl,
|
serverUrl = session.serverUrl,
|
||||||
item = item,
|
item = item,
|
||||||
selected = item.relativePath in state.selectedPaths,
|
selected = item.relativePath in state.selectedPaths,
|
||||||
showExtensions = state.userConfig.showExtensions,
|
showExtensions = state.userConfig.showExtensions,
|
||||||
onSelectedChange = { vm.toggleSelection(item.relativePath) },
|
onSelectedChange = { vm.toggleSelection(item.relativePath) },
|
||||||
onOpenClick = {
|
onOpenClick = { openItem(item) },
|
||||||
when {
|
|
||||||
item.isDirectory -> vm.openFolder(session, item)
|
|
||||||
openable -> vm.openItem(session, item)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onMenuClick = { rect ->
|
onMenuClick = { rect ->
|
||||||
contextMenuItem = item
|
contextMenuItem = item
|
||||||
contextMenuAnchor = rect
|
contextMenuAnchor = rect
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if (index < displayItems.lastIndex) {
|
|
||||||
HorizontalDivider(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
thickness = 1.dp,
|
|
||||||
color = F7Colors.Border.copy(alpha = 0.55f),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user