diff --git a/feature/files/src/main/assets/chrome/comment-icon-black.svg b/feature/files/src/main/assets/chrome/comment-icon-black.svg new file mode 100644 index 0000000..912f948 --- /dev/null +++ b/feature/files/src/main/assets/chrome/comment-icon-black.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/feature/files/src/main/assets/chrome/folder-white.svg b/feature/files/src/main/assets/chrome/folder-white.svg new file mode 100644 index 0000000..17290f8 --- /dev/null +++ b/feature/files/src/main/assets/chrome/folder-white.svg @@ -0,0 +1,4 @@ + + + + diff --git a/feature/files/src/main/assets/chrome/people-icon-black.svg b/feature/files/src/main/assets/chrome/people-icon-black.svg new file mode 100644 index 0000000..b30723d --- /dev/null +++ b/feature/files/src/main/assets/chrome/people-icon-black.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/feature/files/src/main/assets/chrome/plus-white.svg b/feature/files/src/main/assets/chrome/plus-white.svg new file mode 100644 index 0000000..ea9a2b5 --- /dev/null +++ b/feature/files/src/main/assets/chrome/plus-white.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/feature/files/src/main/assets/chrome/star-gray.svg b/feature/files/src/main/assets/chrome/star-gray.svg new file mode 100644 index 0000000..78bb9ba --- /dev/null +++ b/feature/files/src/main/assets/chrome/star-gray.svg @@ -0,0 +1,10 @@ + + + + + + + + + + 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 345cf97..6ab9015 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.heightIn import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -27,11 +28,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape 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.Icon import androidx.compose.material3.CheckboxDefaults @@ -105,6 +102,7 @@ fun FilesNavigationSidebar( onToggleTreePath: (String) -> Unit, onToggleSection: (String) -> Unit, onWebOnlyClick: (String) -> Unit, + onCreateClick: (() -> Unit)? = null, ) { val base = serverUrl.trimEnd('/') AnimatedVisibility( @@ -126,11 +124,18 @@ fun FilesNavigationSidebar( Column( modifier = Modifier .fillMaxHeight() - .fillMaxWidth(0.86f) - .background(F7Colors.Surface) + .fillMaxWidth(0.9f) + .background(F7Colors.Background) .verticalScroll(rememberScrollState()) - .padding(horizontal = 12.dp, vertical = 12.dp), + .padding(start = 10.dp, top = 10.dp, end = 16.dp, bottom = 26.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), ) { + onCreateClick?.let { create -> + FilesSidebarCreateButton( + onClick = create, + modifier = Modifier.padding(bottom = 4.dp), + ) + } FilesSidebarRow( serverUrl = base, iconPath = "files/folder-gray.svg", @@ -233,11 +238,13 @@ fun FilesNavigationSidebar( } if (usage.isNotBlank()) { Spacer(Modifier.height(8.dp)) + // Живая тема: квота по центру, серым 16/20, без прогресс-бара. Text( text = "Использовано $usage", - style = MaterialTheme.typography.bodySmall, + style = MaterialTheme.typography.bodyLarge, color = F7Colors.TextSecondary, - modifier = Modifier.padding(horizontal = 8.dp), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), ) } } @@ -307,6 +314,56 @@ private fun FilesTreeNodeRows( } } +/** + * Градиентная кнопка «Создать или загрузить» сверху панели (живая тема: 40dp, r100, + * градиент 104°, белая папка слева, разделитель и плюс справа). + */ +@Composable +fun FilesSidebarCreateButton(onClick: () -> Unit, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .fillMaxWidth() + .height(40.dp) + .clip(RoundedCornerShape(F7Radius.pill)) + .background( + Brush.linearGradient(listOf(Color(0xFFC0FF7B), Color(0xFF7CBC3D))), + ) + .clickable(onClick = onClick) + .padding(horizontal = 14.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + AsyncImage( + model = FileIcons.chromeAsset("folder-white.svg"), + contentDescription = null, + modifier = Modifier.size(16.dp), + ) + Text( + text = "Создать или загрузить", + modifier = Modifier + .weight(1f) + .padding(horizontal = 12.dp), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + color = Color(0xFFFDFDFD), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Box( + modifier = Modifier + .width(2.dp) + .height(40.dp) + .background(Color.White), + ) + AsyncImage( + model = FileIcons.chromeAsset("plus-white.svg"), + contentDescription = "Создать", + modifier = Modifier + .padding(start = 14.dp) + .size(16.dp), + ) + } +} + @Composable private fun FilesSidebarRow( serverUrl: String, @@ -315,36 +372,34 @@ private fun FilesSidebarRow( selected: Boolean = false, onClick: () -> Unit, ) { + // Живая тема: строка minHeight 40, r8, иконка 20 серым, текст 16/20 #151515, + // активная — фон #ECF9DE; правых «⋮» в навигации нет. Row( modifier = Modifier .fillMaxWidth() + .heightIn(min = 40.dp) .clip(RoundedCornerShape(8.dp)) - .background(if (selected) Color(0xFFE8F5E0) else Color.Transparent) + .background(if (selected) F7Colors.PrimaryLight else Color.Transparent) .clickable(onClick = onClick) - .padding(horizontal = 8.dp, vertical = 10.dp), + .padding(horizontal = 8.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { AsyncImage( model = "$serverUrl/themes/forbion/images/$iconPath", contentDescription = null, - modifier = Modifier.size(18.dp), + modifier = Modifier.size(20.dp), contentScale = ContentScale.Fit, ) - Spacer(Modifier.width(10.dp)) + Spacer(Modifier.width(8.dp)) Text( text = title, modifier = Modifier.weight(1f), - style = MaterialTheme.typography.bodyMedium, - fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + color = F7Colors.TextPrimary, maxLines = 1, overflow = TextOverflow.Ellipsis, ) - AsyncImage( - model = "$serverUrl/themes/forbion/images/files/dots-vertic-gray.svg", - contentDescription = null, - modifier = Modifier.size(16.dp), - contentScale = ContentScale.Fit, - ) } } @@ -366,16 +421,12 @@ private fun FilesSidebarChildRow( Text( text = title, modifier = Modifier.weight(1f), - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + color = F7Colors.TextPrimary, maxLines = 2, overflow = TextOverflow.Ellipsis, ) - AsyncImage( - model = "$serverUrl/themes/forbion/images/files/dots-vertic-gray.svg", - contentDescription = null, - modifier = Modifier.size(16.dp), - contentScale = ContentScale.Fit, - ) } } @@ -392,22 +443,23 @@ private fun FilesSidebarExpandableSection( Row( modifier = Modifier .fillMaxWidth() + .heightIn(min = 40.dp) .clip(RoundedCornerShape(8.dp)) .clickable(onClick = onToggle) - .padding(horizontal = 8.dp, vertical = 10.dp), + .padding(horizontal = 8.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { AsyncImage( model = "$serverUrl/themes/forbion/images/$iconPath", contentDescription = null, - modifier = Modifier.size(18.dp), + modifier = Modifier.size(20.dp), contentScale = ContentScale.Fit, ) - Spacer(Modifier.width(10.dp)) + Spacer(Modifier.width(8.dp)) Text( text = title, modifier = Modifier.weight(1f), - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium, ) Text(text = if (expanded) "▴" else "▾", color = F7Colors.TextSecondary) @@ -744,47 +796,60 @@ private fun FilesHeaderColumn( } } +/** + * Панель выделения по живой теме: 40dp, чекбокс 16 «выбрать все», «Выбрать все» чёрным + + * «(Выбран N)» серым, справа — пилюля «Действия» 25dp (без иконки). + */ @Composable fun FilesBulkActionsBar( serverUrl: String, selectedCount: Int, + allSelected: Boolean = false, + onToggleSelectAll: (() -> Unit)? = null, onActionsClick: (Rect) -> Unit, ) { var anchorBounds by remember { mutableStateOf(null) } - val base = serverUrl.trimEnd('/') Row( modifier = Modifier .fillMaxWidth() - .padding(bottom = 8.dp), + .height(40.dp) + .padding(horizontal = 12.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { + F7ThemeCheckbox(checked = allSelected, onToggle = { onToggleSelectAll?.invoke() }) Text( - text = "Выбрано $selectedCount", + text = buildAnnotatedString { + append("Выбрать все ") + withStyle(SpanStyle(color = F7Colors.TextSecondary)) { + append("(Выбран $selectedCount)") + } + }, + modifier = Modifier.weight(1f), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium, + color = F7Colors.TextPrimary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) - Row( + Box( modifier = Modifier + .height(25.dp) .clip(RoundedCornerShape(100.dp)) + .background(F7Colors.Background) .border(1.dp, F7Colors.Border, RoundedCornerShape(100.dp)) - .clickable { - anchorBounds?.let(onActionsClick) - } + .clickable { anchorBounds?.let(onActionsClick) } .onGloballyPositioned { coords -> anchorBounds = coords.boundsInWindow() } - .padding(horizontal = 12.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp), + .padding(horizontal = 8.dp), + contentAlignment = Alignment.Center, ) { - AsyncImage( - model = "$base/themes/forbion/images/files/dots-vertic-gray.svg", - contentDescription = null, - modifier = Modifier.size(14.dp), - contentScale = ContentScale.Fit, + Text( + "Действия", + style = MaterialTheme.typography.bodyMedium, + color = F7Colors.TextPrimary, ) - Text("Действия", style = MaterialTheme.typography.bodyMedium) } } } @@ -929,12 +994,14 @@ fun FileActionMenuPopup( properties = PopupProperties(focusable = true), ) { Surface( - modifier = Modifier.widthIn(min = 260.dp, max = 320.dp), - shape = RoundedCornerShape(12.dp), - shadowElevation = 8.dp, + modifier = Modifier + .widthIn(min = 280.dp, max = 340.dp) + .border(1.dp, F7Colors.Border, RoundedCornerShape(8.dp)), + shape = RoundedCornerShape(8.dp), + shadowElevation = 4.dp, color = Color.White, ) { - Column(modifier = Modifier.padding(vertical = 6.dp)) { + Column(modifier = Modifier.padding(4.dp)) { entries.forEachIndexed { index, entry -> if (entry.destructive && index > 0) { HorizontalDivider( @@ -945,18 +1012,20 @@ fun FileActionMenuPopup( Row( modifier = Modifier .fillMaxWidth() + .heightIn(min = 25.dp) + .clip(RoundedCornerShape(6.dp)) .clickable { onDismiss() onAction(entry.action) } - .padding(horizontal = 14.dp, vertical = 11.dp), + .padding(horizontal = 8.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { AsyncImage( model = "$base/themes/forbion/images/${entry.iconPath}", contentDescription = null, - modifier = Modifier.size(18.dp), + modifier = Modifier.size(16.dp), contentScale = ContentScale.Fit, ) Text( @@ -1006,12 +1075,14 @@ fun FilesBulkActionMenuPopup( properties = PopupProperties(focusable = true), ) { Surface( - modifier = Modifier.widthIn(min = 240.dp, max = 300.dp), - shape = RoundedCornerShape(12.dp), - shadowElevation = 8.dp, + modifier = Modifier + .widthIn(min = 240.dp, max = 300.dp) + .border(1.dp, F7Colors.Border, RoundedCornerShape(8.dp)), + shape = RoundedCornerShape(8.dp), + shadowElevation = 4.dp, color = Color.White, ) { - Column(modifier = Modifier.padding(vertical = 6.dp)) { + Column(modifier = Modifier.padding(4.dp)) { entries.forEachIndexed { index, (action, label, icon) -> if (index == entries.lastIndex) { HorizontalDivider( @@ -1063,25 +1134,26 @@ fun FilesDetailsSheet( ) { if (item == null) return var commentDraft by remember(item.relativePath) { mutableStateOf("") } - androidx.compose.ui.window.Dialog(onDismissRequest = onDismiss) { + // Живая тема: панель на весь экран, фон #FBFBFB, без скруглений. + androidx.compose.ui.window.Dialog( + onDismissRequest = onDismiss, + properties = androidx.compose.ui.window.DialogProperties(usePlatformDefaultWidth = false), + ) { Surface( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(0.92f), - shape = RoundedCornerShape(16.dp), - color = F7Colors.Surface, + modifier = Modifier.fillMaxSize(), + color = F7Colors.Background, ) { Column( modifier = Modifier .fillMaxSize() - .padding(20.dp), + .padding(horizontal = 16.dp, vertical = 12.dp), ) { - // Крестик закрытия — правый верхний угол (по макету) + // Крестик 40dp — правый верхний угол Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { Box( modifier = Modifier - .size(28.dp) - .clip(CircleShape) + .size(40.dp) + .clip(RoundedCornerShape(8.dp)) .clickable(onClick = onDismiss), contentAlignment = Alignment.Center, ) { @@ -1093,75 +1165,81 @@ fun FilesDetailsSheet( ) } } + // Заголовок 18/600 + звезда 20 справа (живая тема) Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, ) { - Column(modifier = Modifier.weight(1f)) { + Text( + item.name, + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.titleMedium.copy(fontSize = 18.sp), + fontWeight = FontWeight.SemiBold, + color = F7Colors.TextPrimary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + AsyncImage( + model = FileIcons.chromeAsset( + if (item.favorite) "star-green-full.svg" else "star-gray.svg", + ), + contentDescription = "Избранное", + modifier = Modifier + .size(20.dp) + .clickable(onClick = onToggleFavorite), + ) + } + Spacer(Modifier.height(8.dp)) + // Чип владельца (пилюля 25dp) слева, «дата / размер» серым справа + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + modifier = Modifier + .height(25.dp) + .clip(RoundedCornerShape(100.dp)) + .background(F7Colors.Background) + .border(1.dp, F7Colors.Border, RoundedCornerShape(100.dp)) + .padding(horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Box( + modifier = Modifier + .size(16.dp) + .clip(CircleShape) + .background(F7Colors.PrimaryLight), + contentAlignment = Alignment.Center, + ) { + Text( + ownerLabel.firstOrNull()?.uppercaseChar()?.toString() ?: "?", + style = MaterialTheme.typography.labelSmall, + color = F7Colors.Primary, + fontWeight = FontWeight.SemiBold, + ) + } Text( - item.name, - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.SemiBold, - maxLines = 2, + ownerLabel, + style = MaterialTheme.typography.labelMedium, + color = F7Colors.TextPrimary, + maxLines = 1, overflow = TextOverflow.Ellipsis, ) - Spacer(Modifier.height(8.dp)) - // Чип владельца (аватар-инициал + логин) — по макету - Row( - modifier = Modifier - .clip(RoundedCornerShape(100.dp)) - .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, - ) - } - Text( - ownerLabel, - style = MaterialTheme.typography.bodySmall, - color = F7Colors.TextSecondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } } - 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)) } + Spacer(Modifier.weight(1f)) + val dateSize = buildList { + item.lastModified?.let { + add(SimpleDateFormat("d MMMM", Locale("ru")).format(Date(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), + item.size?.let { add(formatFileSize(it)) } + } + if (dateSize.isNotEmpty()) { + Text( + dateSize.joinToString(" / "), + style = MaterialTheme.typography.bodyMedium, + color = F7Colors.TextSecondary, ) } } @@ -1171,14 +1249,14 @@ fun FilesDetailsSheet( horizontalArrangement = Arrangement.spacedBy(12.dp), ) { FilesDetailsTabPill( - icon = Icons.Filled.ChatBubbleOutline, + iconAsset = "comment-icon-black.svg", contentDescription = "Обсуждение", selected = tab == FileDetailsTab.Events, onClick = { onTabChange(FileDetailsTab.Events) }, modifier = Modifier.weight(1f), ) FilesDetailsTabPill( - icon = Icons.Filled.Group, + iconAsset = "people-icon-black.svg", contentDescription = "Общий доступ", selected = tab == FileDetailsTab.Sharing, onClick = { onTabChange(FileDetailsTab.Sharing) }, @@ -1229,27 +1307,31 @@ fun FilesDetailsSheet( @Composable private fun FilesDetailsTabPill( - icon: androidx.compose.ui.graphics.vector.ImageVector, + iconAsset: String, contentDescription: String, selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, ) { - val shape = RoundedCornerShape(14.dp) + // Живая тема: пилюля 30dp r100; активная — фон #ECF9DE, рамка #70B62B 20%; иконка темы 16. + val shape = RoundedCornerShape(100.dp) Box( modifier = modifier - .height(40.dp) + .height(30.dp) .clip(shape) - .background(if (selected) F7Colors.PrimaryLight else F7Colors.Surface) - .border(1.dp, if (selected) Color.Transparent else F7Colors.Border, shape) + .background(if (selected) F7Colors.PrimaryLight else F7Colors.Background) + .border( + 1.dp, + if (selected) F7Colors.Primary.copy(alpha = 0.2f) else F7Colors.Border, + shape, + ) .clickable(onClick = onClick), contentAlignment = Alignment.Center, ) { - Icon( - icon, + AsyncImage( + model = FileIcons.chromeAsset(iconAsset), contentDescription = contentDescription, - tint = if (selected) F7Colors.Primary else F7Colors.TextSecondary, - modifier = Modifier.size(20.dp), + modifier = Modifier.size(16.dp), ) } } diff --git a/feature/files/src/main/java/ru/forbion/f7cloud/feature/files/FilesScreen.kt b/feature/files/src/main/java/ru/forbion/f7cloud/feature/files/FilesScreen.kt index 72ea273..4be5336 100644 --- a/feature/files/src/main/java/ru/forbion/f7cloud/feature/files/FilesScreen.kt +++ b/feature/files/src/main/java/ru/forbion/f7cloud/feature/files/FilesScreen.kt @@ -191,9 +191,8 @@ fun FilesScreen( FilesViewToggleButton(viewMode) { 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() } @@ -203,9 +202,15 @@ fun FilesScreen( onSegmentClick = { i -> vm.navigateToPath(session, crumbSegments.take(i + 1).joinToString("/")) }, ) if (selectionMode) { + val allSelected = displayItems.isNotEmpty() && + state.selectedPaths.size >= displayItems.size FilesBulkActionsBar( serverUrl = session.serverUrl, selectedCount = state.selectedPaths.size, + allSelected = allSelected, + onToggleSelectAll = { + if (allSelected) vm.clearSelection() else vm.selectAllVisible() + }, onActionsClick = { rect -> bulkMenuAnchor = rect bulkMenuOpen = true @@ -323,6 +328,10 @@ fun FilesScreen( vm.showWebOnlyMessage(title) onSidebarOpenChange(false) }, + onCreateClick = { + onSidebarOpenChange(false) + showCreateMenu = true + }, ) FilesCreateMenu(