design(files): навигация, панель выделения, свойства, поповеры — 1:1 по теме

_mobile-files.css, оставшиеся секции:
- Левая панель: ширина 90%, фон #FBFBFB, отступы темы, градиентная
  кнопка «Создать или загрузить» (40dp, 104°, белые папка/плюс/разделитель)
  — кнопка создания из шапки уехала сюда, как в живом; строки 40dp/r8,
  иконка 20, текст 16/20, активная #ECF9DE, без «⋮»; квота по центру серым.
- Панель выделения: 40dp, чекбокс темы «выбрать все» (провязан
  selectAllVisible/clearSelection), «Выбрать все» чёрным + «(Выбран N)»
  серым, пилюля «Действия» 25dp.
- Панель свойств: на весь экран без скруглений, фон #FBFBFB, крестик 40,
  заголовок 18/600 + звезда темы 20 (зелёная/серая), чип владельца-пилюля
  25dp (аватар 16), «дата / размер» серым справа; табы-пилюли 30dp
  с иконками темы (comment/people-icon-black), активная #ECF9DE
  с рамкой #70B62B 20%.
- Поповеры действий (строка и массовые): белые r8 с рамкой и тенью 8%,
  пункты 25dp/r6, иконка 16, текст 14.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
b-dev-mobile
2026-07-09 20:58:28 +00:00
parent 6895f1bffd
commit 1bc568b059
7 changed files with 286 additions and 144 deletions
@@ -0,0 +1,13 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_655_2917)">
<path d="M2.5 5C2.5 3.61929 3.61929 2.5 5 2.5H10H15C16.3807 2.5 17.5 3.61929 17.5 5V12.2727C17.5 13.6534 16.3807 14.7727 15 14.7727H11.6654C11.3957 14.7727 11.1333 14.86 10.9172 15.0214L6.32676 18.4514C6.16189 18.5746 5.92712 18.4569 5.92712 18.2511V15.0227C5.92712 14.8847 5.81519 14.7727 5.67712 14.7727H5C3.61929 14.7727 2.5 13.6534 2.5 12.2727V5Z" stroke="black" stroke-width="1.5"/>
<path d="M6.25 8.75H6.25972" stroke="black" stroke-width="1.875" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.0098 8.75H10.0195" stroke="black" stroke-width="1.875" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.7695 8.75H13.7793" stroke="black" stroke-width="1.875" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_655_2917">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 983 B

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 4.72274V12.6619C15 13.4009 14.4009 14 13.6619 14H2.33813C1.5991 14 1 13.4009 1 12.6619L1 3.33812C1 2.5991 1.5991 2 2.33813 2H8.17409C8.55792 2 8.92327 2.16483 9.17728 2.45259L9.60049 2.93203C9.8545 3.21979 10.2199 3.38462 10.6037 3.38462H13.6619C14.4009 3.38462 15 3.98371 15 4.72274Z" stroke="#ffffffff" stroke-width="1.5"/>
<path d="M1 8.74989V12.6619C1 13.4009 1.5991 14 2.33813 14H13.6619C14.4009 14 15 13.4009 15 12.6619V7.33812C15 6.5991 14.4009 6 13.6619 6H6.86607C6.52921 6 6.20475 6.12705 5.95745 6.35578L5.20044 7.05598C4.95315 7.28472 4.62868 7.41176 4.29183 7.41176H2.33812C1.5991 7.41176 1 8.01086 1 8.74989Z" stroke="#ffffffff" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 779 B

@@ -0,0 +1,13 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_655_7627)">
<path d="M15.3624 8.67973C15.2352 10.4449 13.9258 11.8048 12.4978 11.8048C11.0698 11.8048 9.75817 10.4454 9.63317 8.67973C9.50296 6.84333 10.7777 5.55469 12.4978 5.55469C14.2179 5.55469 15.4926 6.87675 15.3624 8.67973Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.5004 14.584C9.67175 14.584 6.95162 15.989 6.27017 18.7251C6.17989 19.0871 6.50327 19.3756 6.87524 19.3756H18.3187C18.6907 19.3756 18.82 19.0871 18.731 18.7251C18.0496 15.9451 15.3295 14.584 12.5004 14.584Z" stroke="black" stroke-width="1.5" stroke-miterlimit="10"/>
<path d="M8.74367 3.83406C8.63931 5.28263 7.55198 6.42347 6.37993 6.42347C5.20787 6.42347 4.11876 5.28308 4.01618 3.83406C3.90959 2.32706 4.96792 1.25 6.37993 1.25C7.79193 1.25 8.85026 2.35471 8.74367 3.83406Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.75 9.39002C7.97203 8.92775 7.11518 8.75 6.20704 8.75C3.96579 8.75 1.80642 10.1949 1.26551 13.0094C1.19439 13.3816 1.37455 13.75 1.66936 13.75H6.50875" stroke="black" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_655_7627">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_245_63266)">
<path d="M1.5 8L14.5 8" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
<path d="M8 1.5L8 14.5" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_245_63266">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 415 B

@@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_12335_36924)">
<path d="M6.10476 3.60501C6.94943 2.09034 7.37143 1.33301 8.00276 1.33301C8.63409 1.33301 9.05609 2.09034 9.90076 3.60501L10.1194 3.99701C10.3594 4.42767 10.4794 4.64301 10.6661 4.78501C10.8528 4.92701 11.0861 4.97967 11.5528 5.08501L11.9768 5.18101C13.6168 5.55234 14.4361 5.73767 14.6314 6.36501C14.8261 6.99167 14.2674 7.64567 13.1494 8.95301L12.8601 9.29101C12.5428 9.66234 12.3834 9.84834 12.3121 10.0777C12.2408 10.3077 12.2648 10.5557 12.3128 11.051L12.3568 11.5023C12.5254 13.247 12.6101 14.119 12.0994 14.5063C11.5888 14.8937 10.8208 14.5403 9.28609 13.8337L8.88809 13.651C8.45209 13.4497 8.23409 13.3497 8.00276 13.3497C7.77143 13.3497 7.55343 13.4497 7.11743 13.651L6.72009 13.8337C5.18476 14.5403 4.41676 14.8937 3.90676 14.507C3.39543 14.119 3.48009 13.247 3.64876 11.5023L3.69276 11.0517C3.74076 10.5557 3.76476 10.3077 3.69276 10.0783C3.62209 9.84834 3.46276 9.66234 3.14543 9.29167L2.85609 8.95301C1.73809 7.64634 1.17943 6.99234 1.37409 6.36501C1.56876 5.73767 2.38943 5.55167 4.02943 5.18101L4.45343 5.08501C4.91943 4.97967 5.15209 4.92701 5.33943 4.78501C5.52676 4.64301 5.64609 4.42767 5.88609 3.99701L6.10476 3.60501Z" stroke="#808080" stroke-width="1.5"/>
</g>
<defs>
<clipPath id="clip0_12335_36924">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -16,6 +16,7 @@ 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
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@@ -27,11 +28,7 @@ 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.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward 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.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.Icon
import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.CheckboxDefaults
@@ -105,6 +102,7 @@ fun FilesNavigationSidebar(
onToggleTreePath: (String) -> Unit, onToggleTreePath: (String) -> Unit,
onToggleSection: (String) -> Unit, onToggleSection: (String) -> Unit,
onWebOnlyClick: (String) -> Unit, onWebOnlyClick: (String) -> Unit,
onCreateClick: (() -> Unit)? = null,
) { ) {
val base = serverUrl.trimEnd('/') val base = serverUrl.trimEnd('/')
AnimatedVisibility( AnimatedVisibility(
@@ -126,11 +124,18 @@ fun FilesNavigationSidebar(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxHeight() .fillMaxHeight()
.fillMaxWidth(0.86f) .fillMaxWidth(0.9f)
.background(F7Colors.Surface) .background(F7Colors.Background)
.verticalScroll(rememberScrollState()) .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( FilesSidebarRow(
serverUrl = base, serverUrl = base,
iconPath = "files/folder-gray.svg", iconPath = "files/folder-gray.svg",
@@ -233,11 +238,13 @@ fun FilesNavigationSidebar(
} }
if (usage.isNotBlank()) { if (usage.isNotBlank()) {
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
// Живая тема: квота по центру, серым 16/20, без прогресс-бара.
Text( Text(
text = "Использовано $usage", text = "Использовано $usage",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodyLarge,
color = F7Colors.TextSecondary, 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 @Composable
private fun FilesSidebarRow( private fun FilesSidebarRow(
serverUrl: String, serverUrl: String,
@@ -315,36 +372,34 @@ private fun FilesSidebarRow(
selected: Boolean = false, selected: Boolean = false,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
// Живая тема: строка minHeight 40, r8, иконка 20 серым, текст 16/20 #151515,
// активная — фон #ECF9DE; правых «⋮» в навигации нет.
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.heightIn(min = 40.dp)
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(8.dp))
.background(if (selected) Color(0xFFE8F5E0) else Color.Transparent) .background(if (selected) F7Colors.PrimaryLight else Color.Transparent)
.clickable(onClick = onClick) .clickable(onClick = onClick)
.padding(horizontal = 8.dp, vertical = 10.dp), .padding(horizontal = 8.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
AsyncImage( AsyncImage(
model = "$serverUrl/themes/forbion/images/$iconPath", model = "$serverUrl/themes/forbion/images/$iconPath",
contentDescription = null, contentDescription = null,
modifier = Modifier.size(18.dp), modifier = Modifier.size(20.dp),
contentScale = ContentScale.Fit, contentScale = ContentScale.Fit,
) )
Spacer(Modifier.width(10.dp)) Spacer(Modifier.width(8.dp))
Text( Text(
text = title, text = title,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyLarge,
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal, fontWeight = FontWeight.Medium,
color = F7Colors.TextPrimary,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, 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(
text = title, text = title,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium,
color = F7Colors.TextPrimary,
maxLines = 2, maxLines = 2,
overflow = TextOverflow.Ellipsis, 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( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.heightIn(min = 40.dp)
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(8.dp))
.clickable(onClick = onToggle) .clickable(onClick = onToggle)
.padding(horizontal = 8.dp, vertical = 10.dp), .padding(horizontal = 8.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
AsyncImage( AsyncImage(
model = "$serverUrl/themes/forbion/images/$iconPath", model = "$serverUrl/themes/forbion/images/$iconPath",
contentDescription = null, contentDescription = null,
modifier = Modifier.size(18.dp), modifier = Modifier.size(20.dp),
contentScale = ContentScale.Fit, contentScale = ContentScale.Fit,
) )
Spacer(Modifier.width(10.dp)) Spacer(Modifier.width(8.dp))
Text( Text(
text = title, text = title,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
) )
Text(text = if (expanded) "" else "", color = F7Colors.TextSecondary) Text(text = if (expanded) "" else "", color = F7Colors.TextSecondary)
@@ -744,47 +796,60 @@ private fun FilesHeaderColumn(
} }
} }
/**
* Панель выделения по живой теме: 40dp, чекбокс 16 «выбрать все», «Выбрать все» чёрным +
* «(Выбран N)» серым, справа — пилюля «Действия» 25dp (без иконки).
*/
@Composable @Composable
fun FilesBulkActionsBar( fun FilesBulkActionsBar(
serverUrl: String, serverUrl: String,
selectedCount: Int, selectedCount: Int,
allSelected: Boolean = false,
onToggleSelectAll: (() -> Unit)? = null,
onActionsClick: (Rect) -> Unit, onActionsClick: (Rect) -> Unit,
) { ) {
var anchorBounds by remember { mutableStateOf<Rect?>(null) } var anchorBounds by remember { mutableStateOf<Rect?>(null) }
val base = serverUrl.trimEnd('/')
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 8.dp), .height(40.dp)
.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
F7ThemeCheckbox(checked = allSelected, onToggle = { onToggleSelectAll?.invoke() })
Text( Text(
text = "Выбрано $selectedCount", text = buildAnnotatedString {
append("Выбрать все ")
withStyle(SpanStyle(color = F7Colors.TextSecondary)) {
append("(Выбран $selectedCount)")
}
},
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
color = F7Colors.TextPrimary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
) )
Row( Box(
modifier = Modifier modifier = Modifier
.height(25.dp)
.clip(RoundedCornerShape(100.dp)) .clip(RoundedCornerShape(100.dp))
.background(F7Colors.Background)
.border(1.dp, F7Colors.Border, RoundedCornerShape(100.dp)) .border(1.dp, F7Colors.Border, RoundedCornerShape(100.dp))
.clickable { .clickable { anchorBounds?.let(onActionsClick) }
anchorBounds?.let(onActionsClick)
}
.onGloballyPositioned { coords -> .onGloballyPositioned { coords ->
anchorBounds = coords.boundsInWindow() anchorBounds = coords.boundsInWindow()
} }
.padding(horizontal = 12.dp, vertical = 8.dp), .padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically, contentAlignment = Alignment.Center,
horizontalArrangement = Arrangement.spacedBy(6.dp),
) { ) {
AsyncImage( Text(
model = "$base/themes/forbion/images/files/dots-vertic-gray.svg", "Действия",
contentDescription = null, style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.size(14.dp), color = F7Colors.TextPrimary,
contentScale = ContentScale.Fit,
) )
Text("Действия", style = MaterialTheme.typography.bodyMedium)
} }
} }
} }
@@ -929,12 +994,14 @@ fun FileActionMenuPopup(
properties = PopupProperties(focusable = true), properties = PopupProperties(focusable = true),
) { ) {
Surface( Surface(
modifier = Modifier.widthIn(min = 260.dp, max = 320.dp), modifier = Modifier
shape = RoundedCornerShape(12.dp), .widthIn(min = 280.dp, max = 340.dp)
shadowElevation = 8.dp, .border(1.dp, F7Colors.Border, RoundedCornerShape(8.dp)),
shape = RoundedCornerShape(8.dp),
shadowElevation = 4.dp,
color = Color.White, color = Color.White,
) { ) {
Column(modifier = Modifier.padding(vertical = 6.dp)) { Column(modifier = Modifier.padding(4.dp)) {
entries.forEachIndexed { index, entry -> entries.forEachIndexed { index, entry ->
if (entry.destructive && index > 0) { if (entry.destructive && index > 0) {
HorizontalDivider( HorizontalDivider(
@@ -945,18 +1012,20 @@ fun FileActionMenuPopup(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.heightIn(min = 25.dp)
.clip(RoundedCornerShape(6.dp))
.clickable { .clickable {
onDismiss() onDismiss()
onAction(entry.action) onAction(entry.action)
} }
.padding(horizontal = 14.dp, vertical = 11.dp), .padding(horizontal = 8.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
AsyncImage( AsyncImage(
model = "$base/themes/forbion/images/${entry.iconPath}", model = "$base/themes/forbion/images/${entry.iconPath}",
contentDescription = null, contentDescription = null,
modifier = Modifier.size(18.dp), modifier = Modifier.size(16.dp),
contentScale = ContentScale.Fit, contentScale = ContentScale.Fit,
) )
Text( Text(
@@ -1006,12 +1075,14 @@ fun FilesBulkActionMenuPopup(
properties = PopupProperties(focusable = true), properties = PopupProperties(focusable = true),
) { ) {
Surface( Surface(
modifier = Modifier.widthIn(min = 240.dp, max = 300.dp), modifier = Modifier
shape = RoundedCornerShape(12.dp), .widthIn(min = 240.dp, max = 300.dp)
shadowElevation = 8.dp, .border(1.dp, F7Colors.Border, RoundedCornerShape(8.dp)),
shape = RoundedCornerShape(8.dp),
shadowElevation = 4.dp,
color = Color.White, color = Color.White,
) { ) {
Column(modifier = Modifier.padding(vertical = 6.dp)) { Column(modifier = Modifier.padding(4.dp)) {
entries.forEachIndexed { index, (action, label, icon) -> entries.forEachIndexed { index, (action, label, icon) ->
if (index == entries.lastIndex) { if (index == entries.lastIndex) {
HorizontalDivider( HorizontalDivider(
@@ -1063,25 +1134,26 @@ fun FilesDetailsSheet(
) { ) {
if (item == null) return if (item == null) return
var commentDraft by remember(item.relativePath) { mutableStateOf("") } 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( Surface(
modifier = Modifier modifier = Modifier.fillMaxSize(),
.fillMaxWidth() color = F7Colors.Background,
.fillMaxHeight(0.92f),
shape = RoundedCornerShape(16.dp),
color = F7Colors.Surface,
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(20.dp), .padding(horizontal = 16.dp, vertical = 12.dp),
) { ) {
// Крестик закрытия — правый верхний угол (по макету) // Крестик 40dp — правый верхний угол
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
Box( Box(
modifier = Modifier modifier = Modifier
.size(28.dp) .size(40.dp)
.clip(CircleShape) .clip(RoundedCornerShape(8.dp))
.clickable(onClick = onDismiss), .clickable(onClick = onDismiss),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
@@ -1093,32 +1165,50 @@ fun FilesDetailsSheet(
) )
} }
} }
// Заголовок 18/600 + звезда 20 справа (живая тема)
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.Top, verticalAlignment = Alignment.CenterVertically,
) { ) {
Column(modifier = Modifier.weight(1f)) {
Text( Text(
item.name, item.name,
style = MaterialTheme.typography.titleLarge, modifier = Modifier.weight(1f),
style = MaterialTheme.typography.titleMedium.copy(fontSize = 18.sp),
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
maxLines = 2, color = F7Colors.TextPrimary,
maxLines = 1,
overflow = TextOverflow.Ellipsis, 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)) Spacer(Modifier.height(8.dp))
// Чип владельца (аватар-инициал + логин) — по макету // Чип владельца (пилюля 25dp) слева, «дата / размер» серым справа
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
Row( Row(
modifier = Modifier modifier = Modifier
.height(25.dp)
.clip(RoundedCornerShape(100.dp)) .clip(RoundedCornerShape(100.dp))
.background(F7Colors.SurfaceMuted) .background(F7Colors.Background)
.padding(start = 4.dp, end = 10.dp, top = 4.dp, bottom = 4.dp), .border(1.dp, F7Colors.Border, RoundedCornerShape(100.dp))
.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp), horizontalArrangement = Arrangement.spacedBy(4.dp),
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.size(20.dp) .size(16.dp)
.clip(CircleShape) .clip(CircleShape)
.background(F7Colors.PrimaryLight), .background(F7Colors.PrimaryLight),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
@@ -1132,14 +1222,13 @@ fun FilesDetailsSheet(
} }
Text( Text(
ownerLabel, ownerLabel,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.labelMedium,
color = F7Colors.TextSecondary, color = F7Colors.TextPrimary,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
} }
} Spacer(Modifier.weight(1f))
Column(horizontalAlignment = Alignment.End) {
val dateSize = buildList { val dateSize = buildList {
item.lastModified?.let { item.lastModified?.let {
add(SimpleDateFormat("d MMMM", Locale("ru")).format(Date(it))) add(SimpleDateFormat("d MMMM", Locale("ru")).format(Date(it)))
@@ -1149,20 +1238,9 @@ fun FilesDetailsSheet(
if (dateSize.isNotEmpty()) { if (dateSize.isNotEmpty()) {
Text( Text(
dateSize.joinToString(" / "), dateSize.joinToString(" / "),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodyMedium,
color = F7Colors.TextSecondary, 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))
@@ -1171,14 +1249,14 @@ fun FilesDetailsSheet(
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
) { ) {
FilesDetailsTabPill( FilesDetailsTabPill(
icon = Icons.Filled.ChatBubbleOutline, iconAsset = "comment-icon-black.svg",
contentDescription = "Обсуждение", contentDescription = "Обсуждение",
selected = tab == FileDetailsTab.Events, selected = tab == FileDetailsTab.Events,
onClick = { onTabChange(FileDetailsTab.Events) }, onClick = { onTabChange(FileDetailsTab.Events) },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
FilesDetailsTabPill( FilesDetailsTabPill(
icon = Icons.Filled.Group, iconAsset = "people-icon-black.svg",
contentDescription = "Общий доступ", contentDescription = "Общий доступ",
selected = tab == FileDetailsTab.Sharing, selected = tab == FileDetailsTab.Sharing,
onClick = { onTabChange(FileDetailsTab.Sharing) }, onClick = { onTabChange(FileDetailsTab.Sharing) },
@@ -1229,27 +1307,31 @@ fun FilesDetailsSheet(
@Composable @Composable
private fun FilesDetailsTabPill( private fun FilesDetailsTabPill(
icon: androidx.compose.ui.graphics.vector.ImageVector, iconAsset: String,
contentDescription: String, contentDescription: String,
selected: Boolean, selected: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val shape = RoundedCornerShape(14.dp) // Живая тема: пилюля 30dp r100; активная — фон #ECF9DE, рамка #70B62B 20%; иконка темы 16.
val shape = RoundedCornerShape(100.dp)
Box( Box(
modifier = modifier modifier = modifier
.height(40.dp) .height(30.dp)
.clip(shape) .clip(shape)
.background(if (selected) F7Colors.PrimaryLight else F7Colors.Surface) .background(if (selected) F7Colors.PrimaryLight else F7Colors.Background)
.border(1.dp, if (selected) Color.Transparent else F7Colors.Border, shape) .border(
1.dp,
if (selected) F7Colors.Primary.copy(alpha = 0.2f) else F7Colors.Border,
shape,
)
.clickable(onClick = onClick), .clickable(onClick = onClick),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
Icon( AsyncImage(
icon, model = FileIcons.chromeAsset(iconAsset),
contentDescription = contentDescription, contentDescription = contentDescription,
tint = if (selected) F7Colors.Primary else F7Colors.TextSecondary, modifier = Modifier.size(16.dp),
modifier = Modifier.size(20.dp),
) )
} }
} }
@@ -191,9 +191,8 @@ 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() }
@@ -203,9 +202,15 @@ fun FilesScreen(
onSegmentClick = { i -> vm.navigateToPath(session, crumbSegments.take(i + 1).joinToString("/")) }, onSegmentClick = { i -> vm.navigateToPath(session, crumbSegments.take(i + 1).joinToString("/")) },
) )
if (selectionMode) { if (selectionMode) {
val allSelected = displayItems.isNotEmpty() &&
state.selectedPaths.size >= displayItems.size
FilesBulkActionsBar( FilesBulkActionsBar(
serverUrl = session.serverUrl, serverUrl = session.serverUrl,
selectedCount = state.selectedPaths.size, selectedCount = state.selectedPaths.size,
allSelected = allSelected,
onToggleSelectAll = {
if (allSelected) vm.clearSelection() else vm.selectAllVisible()
},
onActionsClick = { rect -> onActionsClick = { rect ->
bulkMenuAnchor = rect bulkMenuAnchor = rect
bulkMenuOpen = true bulkMenuOpen = true
@@ -323,6 +328,10 @@ fun FilesScreen(
vm.showWebOnlyMessage(title) vm.showWebOnlyMessage(title)
onSidebarOpenChange(false) onSidebarOpenChange(false)
}, },
onCreateClick = {
onSidebarOpenChange(false)
showCreateMenu = true
},
) )
FilesCreateMenu( FilesCreateMenu(