files: рескин панели свойств/доступа под макет (табы-пилюли, шапка, звезда)

Правая панель (FilesDetailsSheet) по макету «Правая панель свойства/доступ»:
- табы из текст+подчёркивание → две иконки-пилюли (Обсуждение 💬 / Доступ 👥),
  активная — зелёная заливка PrimaryLight;
- шапка: крестик в правом верхнем углу, чип владельца (аватар-инициал+логин),
  дата/размер справа, звезда-избранное (Icons Star/StarBorder, wired на
  vm.toggleFavorite) вместо строки «Владелец … • …»;
- кнопка отправки комментария → зелёный круг-градиент с белой стрелкой (было «→»).
Иконки — Material (extended уже в модуле). Компилируется.
This commit is contained in:
b-dev-mobile
2026-07-08 10:11:57 +00:00
parent b15a97d30e
commit 335468b80f
2 changed files with 132 additions and 68 deletions
@@ -21,9 +21,18 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape 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.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.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
@@ -49,6 +58,7 @@ import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.PlatformTextStyle import androidx.compose.ui.text.PlatformTextStyle
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.boundsInWindow
@@ -998,10 +1008,10 @@ fun FilesDetailsSheet(
loading: Boolean, loading: Boolean,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onTabChange: (FileDetailsTab) -> Unit, onTabChange: (FileDetailsTab) -> Unit,
onToggleFavorite: () -> Unit,
onWebOnlyAction: (String) -> Unit, onWebOnlyAction: (String) -> Unit,
) { ) {
if (item == null) return if (item == null) return
val base = serverUrl.trimEnd('/')
var commentDraft by remember(item.relativePath) { mutableStateOf("") } var commentDraft by remember(item.relativePath) { mutableStateOf("") }
androidx.compose.ui.window.Dialog(onDismissRequest = onDismiss) { androidx.compose.ui.window.Dialog(onDismissRequest = onDismiss) {
Surface( Surface(
@@ -1016,9 +1026,26 @@ fun FilesDetailsSheet(
.fillMaxSize() .fillMaxSize()
.padding(20.dp), .padding(20.dp),
) { ) {
// Крестик закрытия — правый верхний угол (по макету)
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
Box(
modifier = Modifier
.size(28.dp)
.clip(CircleShape)
.clickable(onClick = onDismiss),
contentAlignment = Alignment.Center,
) {
Icon(
Icons.Filled.Close,
contentDescription = "Закрыть",
tint = F7Colors.TextSecondary,
modifier = Modifier.size(20.dp),
)
}
}
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.Top, verticalAlignment = Alignment.Top,
) { ) {
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
@@ -1029,52 +1056,86 @@ fun FilesDetailsSheet(
maxLines = 2, maxLines = 2,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
Spacer(Modifier.height(6.dp)) Spacer(Modifier.height(8.dp))
val metaParts = buildList { // Чип владельца (аватар-инициал + логин) — по макету
item.size?.let { add(formatFileSize(it)) } Row(
item.lastModified?.let { modifier = Modifier
add( .clip(RoundedCornerShape(100.dp))
SimpleDateFormat("d MMMM", Locale("ru")).format(Date(it)), .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,
) )
} }
add("Владелец $ownerLabel") Text(
ownerLabel,
style = MaterialTheme.typography.bodySmall,
color = F7Colors.TextSecondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
} }
Text(
metaParts.joinToString(""),
style = MaterialTheme.typography.bodySmall,
color = F7Colors.TextSecondary,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
} }
TextButton(onClick = onDismiss) { Column(horizontalAlignment = Alignment.End) {
Text("", style = MaterialTheme.typography.titleMedium) val dateSize = buildList {
item.lastModified?.let {
add(SimpleDateFormat("d MMMM", Locale("ru")).format(Date(it)))
}
item.size?.let { add(formatFileSize(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),
)
} }
} }
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(24.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
) { ) {
FilesDetailsTabButton( FilesDetailsTabPill(
serverUrl = serverUrl, icon = Icons.Filled.ChatBubbleOutline,
label = "Общий доступ", contentDescription = "Обсуждение",
iconPath = "files/many-person-gray.svg",
selected = tab == FileDetailsTab.Sharing,
onClick = { onTabChange(FileDetailsTab.Sharing) },
)
FilesDetailsTabButton(
serverUrl = serverUrl,
label = "События",
iconPath = "files/grid-files-gray.svg",
selected = tab == FileDetailsTab.Events, selected = tab == FileDetailsTab.Events,
onClick = { onTabChange(FileDetailsTab.Events) }, onClick = { onTabChange(FileDetailsTab.Events) },
modifier = Modifier.weight(1f),
)
FilesDetailsTabPill(
icon = Icons.Filled.Group,
contentDescription = "Общий доступ",
selected = tab == FileDetailsTab.Sharing,
onClick = { onTabChange(FileDetailsTab.Sharing) },
modifier = Modifier.weight(1f),
) )
} }
HorizontalDivider( Spacer(Modifier.height(16.dp))
modifier = Modifier.padding(top = 8.dp),
color = F7Colors.Border.copy(alpha = 0.5f),
)
Box( Box(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
@@ -1117,42 +1178,28 @@ fun FilesDetailsSheet(
} }
@Composable @Composable
private fun FilesDetailsTabButton( private fun FilesDetailsTabPill(
serverUrl: String, icon: androidx.compose.ui.graphics.vector.ImageVector,
label: String, contentDescription: String,
iconPath: String,
selected: Boolean, selected: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
val base = serverUrl.trimEnd('/') val shape = RoundedCornerShape(14.dp)
Column( Box(
modifier = Modifier.clickable(onClick = onClick), modifier = modifier
horizontalAlignment = Alignment.CenterHorizontally, .height(40.dp)
.clip(shape)
.background(if (selected) F7Colors.PrimaryLight else F7Colors.Surface)
.border(1.dp, if (selected) Color.Transparent else F7Colors.Border, shape)
.clickable(onClick = onClick),
contentAlignment = Alignment.Center,
) { ) {
Row( Icon(
verticalAlignment = Alignment.CenterVertically, icon,
horizontalArrangement = Arrangement.spacedBy(6.dp), contentDescription = contentDescription,
) { tint = if (selected) F7Colors.Primary else F7Colors.TextSecondary,
AsyncImage( modifier = Modifier.size(20.dp),
model = "$base/themes/forbion/images/$iconPath",
contentDescription = null,
modifier = Modifier.size(16.dp),
contentScale = ContentScale.Fit,
)
Text(
label,
style = MaterialTheme.typography.bodyMedium,
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Medium,
color = if (selected) F7Colors.TextPrimary else F7Colors.TextSecondary,
)
}
Spacer(Modifier.height(8.dp))
Box(
modifier = Modifier
.height(2.dp)
.width(72.dp)
.clip(RoundedCornerShape(1.dp))
.background(if (selected) F7Colors.Primary else Color.Transparent),
) )
} }
} }
@@ -1348,8 +1395,24 @@ private fun FilesEventsTabContent(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
onEnter = onSubmitComment, onEnter = onSubmitComment,
) )
TextButton(onClick = onSubmitComment) { Box(
Text("", fontSize = 20.sp) modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(
Brush.linearGradient(
listOf(F7Colors.PrimaryGradientStart, F7Colors.PrimaryGradientEnd),
),
)
.clickable(onClick = onSubmitComment),
contentAlignment = Alignment.Center,
) {
Icon(
Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = "Отправить",
tint = Color.White,
modifier = Modifier.size(18.dp),
)
} }
} }
Text( Text(
@@ -360,6 +360,7 @@ fun FilesScreen(
loading = state.detailsLoading, loading = state.detailsLoading,
onDismiss = vm::hideDetails, onDismiss = vm::hideDetails,
onTabChange = vm::setDetailsTab, onTabChange = vm::setDetailsTab,
onToggleFavorite = { state.detailsItem?.let { vm.toggleFavorite(session, it) } },
onWebOnlyAction = vm::showWebOnlyMessage, onWebOnlyAction = vm::showWebOnlyMessage,
) )