design(files): главный экран 1:1 по живой теме forbion (_mobile-files.css)
Источник — мобильный CSS боевого сервера (копия в design-reference). - 9 фирменных SVG хрома забандлены в assets/chrome/ (офлайн): чекбоксы 16dp (контур/зелёный), звезда избранного, ⋮, лупа, дом, тумблеры вида, цветная папка (закрыт TODO «папка с сервера»). - Строка списка: высота 44dp, чекбокс темы 16 в колонке 44, иконка 30 (было 24) + звезда-оверлей у избранных, колонки размера (56, центр) и даты (88, вправо, относительное время как в вебе), ⋮ 16 в колонке 32, без скругления, выбранная — #ECF9DE. - Тумблер вида: круг 40, иконка 16 из темы. Крошки: дом 16 (home-bottom), все сегменты серые 14/20, шеврон 16. Фильтр-чипы: пилюли 25dp, minWidth 86, текст чёрный, фон #FBFBFB + мягкая тень (как в теме). - Карточка сетки: радиус 12, иконка 80 (было 48), чекбокс/звезда 16 по углам, ⋮ 16, фон #FBFBFB. - Поиск: фон #FBFBFB, локальная лупа из CSS темы, крестик-очистка 20dp. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@@ -0,0 +1,15 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<g clip-path="url(#clip0_3969_9720)">
|
||||||
|
<g clip-path="url(#clip1_3969_9720)">
|
||||||
|
<rect x="1.75" y="1.75" width="12.5" height="12.5" rx="2.25" stroke="#808080" stroke-width="1.5"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_3969_9720">
|
||||||
|
<rect width="16" height="16" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="clip1_3969_9720">
|
||||||
|
<rect width="16" height="16" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 592 B |
@@ -0,0 +1,16 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<g clip-path="url(#clip0_3969_9737)">
|
||||||
|
<g clip-path="url(#clip1_3969_9737)">
|
||||||
|
<rect x="1.75" y="1.75" width="12.5" height="12.5" rx="2.25" fill="#70B62B" stroke="#70B62B" stroke-width="1.5"/>
|
||||||
|
<path d="M4 9.14286L7.80954 11.864C7.90955 11.9354 8.04971 11.9006 8.10467 11.7907L12 4" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_3969_9737">
|
||||||
|
<rect width="16" height="16" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="clip1_3969_9737">
|
||||||
|
<rect width="16" height="16" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 768 B |
@@ -0,0 +1,12 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_10995_59807)">
|
||||||
|
<circle cx="8.5" cy="13.5" r="1.5" transform="rotate(-90 8.5 13.5)" fill="#808080"/>
|
||||||
|
<circle cx="8.5" cy="8.5" r="1.5" transform="rotate(-90 8.5 8.5)" fill="#808080"/>
|
||||||
|
<circle cx="8.5" cy="3.5" r="1.5" transform="rotate(-90 8.5 3.5)" fill="#808080"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_10995_59807">
|
||||||
|
<rect width="16" height="16" fill="white" transform="matrix(0 -1 1 0 0 16)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 538 B |
@@ -0,0 +1,17 @@
|
|||||||
|
<svg width="75" height="75" viewBox="0 0 75 75" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_17_4782)">
|
||||||
|
<circle cx="37.5" cy="37.5" r="37.5" fill="#E6E6E6"/>
|
||||||
|
<path d="M61.875 32.8754V45.3395C61.875 48.1108 59.6284 50.3574 56.857 50.3574H16.268C13.4966 50.3574 11.25 48.1108 11.25 45.3395V27.2504C11.25 24.479 13.4966 22.2324 16.268 22.2324H37.2965C38.6273 22.2324 39.9037 22.7611 40.8447 23.7021L43.5303 26.3877C44.4713 27.3287 45.7477 27.8574 47.0785 27.8574H56.857C59.6284 27.8574 61.875 30.104 61.875 32.8754Z" fill="#F5F5F5" stroke="black" stroke-width="1.67266"/>
|
||||||
|
<path d="M18.75 10.9824V50.3574H56.25V10.9824H18.75Z" fill="#F5F5F5" stroke="black" stroke-width="1.67266"/>
|
||||||
|
<path d="M16.875 14.7324V55.9824H52.5V14.7324H16.875Z" fill="#FCFEFC" stroke="black" stroke-width="1.67266"/>
|
||||||
|
<path d="M11.25 42.2504V58.4645C11.25 61.2358 13.4966 63.4824 16.268 63.4824H58.732C61.5034 63.4824 63.75 61.2358 63.75 58.4645V36.6254C63.75 33.854 61.5034 31.6074 58.732 31.6074H33.3359C32.0202 31.6074 30.7571 32.1242 29.8186 33.0464L27.0235 35.7934C26.085 36.7157 24.8219 37.2324 23.5062 37.2324H16.268C13.4966 37.2324 11.25 39.479 11.25 42.2504Z" fill="#F5F5F5" stroke="black" stroke-width="1.67266"/>
|
||||||
|
<path d="M20.625 20.3574H48.75" stroke="#70B62B" stroke-width="1.67266" stroke-linecap="square"/>
|
||||||
|
<path d="M20.625 24.1074H48.75" stroke="#70B62B" stroke-width="1.67266" stroke-linecap="square"/>
|
||||||
|
<path d="M20.625 27.8574H48.75" stroke="#70B62B" stroke-width="1.67266" stroke-linecap="square"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_17_4782">
|
||||||
|
<rect width="75" height="75" rx="15" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_17_4585)">
|
||||||
|
<path d="M4.25 2L4.25 6C4.25 6.13807 4.13807 6.25 4 6.25L2 6.25C1.86193 6.25 1.75 6.13807 1.75 6L1.75 2C1.75 1.86193 1.86193 1.75 2 1.75L4 1.75C4.13807 1.75 4.25 1.86193 4.25 2Z" stroke="#808080" stroke-width="1.5"/>
|
||||||
|
<path d="M4.25 10L4.25 14C4.25 14.1381 4.13807 14.25 4 14.25H2C1.86193 14.25 1.75 14.1381 1.75 14L1.75 10C1.75 9.86193 1.86193 9.75 2 9.75H4C4.13807 9.75 4.25 9.86193 4.25 10Z" stroke="#808080" stroke-width="1.5"/>
|
||||||
|
<path d="M9.25 2V6C9.25 6.13807 9.13807 6.25 9 6.25L7 6.25C6.86193 6.25 6.75 6.13807 6.75 6L6.75 2C6.75 1.86193 6.86193 1.75 7 1.75L9 1.75C9.13807 1.75 9.25 1.86193 9.25 2Z" stroke="#808080" stroke-width="1.5"/>
|
||||||
|
<path d="M9.25 10V14C9.25 14.1381 9.13807 14.25 9 14.25H7C6.86193 14.25 6.75 14.1381 6.75 14L6.75 10C6.75 9.86193 6.86193 9.75 7 9.75H9C9.13807 9.75 9.25 9.86193 9.25 10Z" stroke="#808080" stroke-width="1.5"/>
|
||||||
|
<path d="M14.25 2V6C14.25 6.13807 14.1381 6.25 14 6.25L12 6.25C11.8619 6.25 11.75 6.13807 11.75 6V2C11.75 1.86193 11.8619 1.75 12 1.75L14 1.75C14.1381 1.75 14.25 1.86193 14.25 2Z" stroke="#808080" stroke-width="1.5"/>
|
||||||
|
<path d="M14.25 10V14C14.25 14.1381 14.1381 14.25 14 14.25H12C11.8619 14.25 11.75 14.1381 11.75 14V10C11.75 9.86193 11.8619 9.75 12 9.75H14C14.1381 9.75 14.25 9.86193 14.25 10Z" stroke="#808080" stroke-width="1.5"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_17_4585">
|
||||||
|
<rect width="16" height="16" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_1460_24542)">
|
||||||
|
<g clip-path="url(#clip1_1460_24542)">
|
||||||
|
<g clip-path="url(#clip2_1460_24542)">
|
||||||
|
<path d="M10.2102 14.9989V9.10405C10.2102 8.90862 10.1326 8.7212 9.99441 8.58301C9.85623 8.44482 9.6688 8.36719 9.47337 8.36719H6.52592C6.3305 8.36719 6.14307 8.44482 6.00488 8.58301C5.8667 8.7212 5.78906 8.90862 5.78906 9.10405V14.9989" stroke="#808080" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M1.36719 6.89451C1.36714 6.68014 1.41386 6.46833 1.50409 6.27387C1.59432 6.07941 1.72589 5.90697 1.88962 5.76859L7.04766 1.34815C7.31366 1.12334 7.65068 1 7.99895 1C8.34722 1 8.68424 1.12334 8.95024 1.34815L14.1083 5.76859C14.272 5.90697 14.4036 6.07941 14.4938 6.27387C14.584 6.46833 14.6308 6.68014 14.6307 6.89451V13.5263C14.6307 13.9171 14.4754 14.292 14.1991 14.5684C13.9227 14.8447 13.5478 15 13.157 15H2.84091C2.45006 15 2.07521 14.8447 1.79883 14.5684C1.52245 14.292 1.36719 13.9171 1.36719 13.5263V6.89451Z" stroke="#808080" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1460_24542">
|
||||||
|
<rect width="16" height="16" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="clip1_1460_24542">
|
||||||
|
<rect width="16" height="16" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="clip2_1460_24542">
|
||||||
|
<rect width="16" height="16" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_5015_21462)">
|
||||||
|
<rect x="1" y="2" width="3" height="3" fill="#151515"/>
|
||||||
|
<path d="M6 3.5L14 3.5" stroke="#151515" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<rect x="1" y="7" width="3" height="3" fill="#151515"/>
|
||||||
|
<path d="M6 8.5L14 8.5" stroke="#151515" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<rect x="1" y="12" width="3" height="3" fill="#151515"/>
|
||||||
|
<path d="M6 13.5L14 13.5" stroke="#151515" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_5015_21462">
|
||||||
|
<rect width="16" height="16" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 749 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0)"><circle cx="5.5" cy="5.5" r="4.75" stroke="#808080" stroke-width="1.5"/><path d="M8.73541 9L14.7354 15" stroke="#808080" stroke-width="1.5" stroke-linecap="round"/></g><defs><clipPath id="clip0"><rect width="16" height="16" fill="white"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 385 B |
@@ -0,0 +1,10 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="#70B62B" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_11908_43900)">
|
||||||
|
<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="#70B62B" stroke-width="1.5"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_11908_43900">
|
||||||
|
<rect width="16" height="16" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -1,13 +1,16 @@
|
|||||||
package ru.forbion.f7cloud.feature.files
|
package ru.forbion.f7cloud.feature.files
|
||||||
|
|
||||||
object FileIcons {
|
object FileIcons {
|
||||||
|
/** Локальные SVG хрома Файлов из боевой темы forbion (assets/chrome/, coil-svg). */
|
||||||
|
fun chromeAsset(name: String): String = "file:///android_asset/chrome/$name"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Иконка типа файла. Типы файлов — ЛОКАЛЬНЫЕ SVG из дизайна (assets/filetypes/, грузятся
|
* Иконка типа файла. Типы файлов — ЛОКАЛЬНЫЕ SVG из дизайна (assets/filetypes/, грузятся
|
||||||
* coil-svg): офлайн + точные иконки макета (раньше тянулись с сервера — аудит: офлайн ломался).
|
* coil-svg): офлайн + точные иконки макета (раньше тянулись с сервера — аудит: офлайн ломался).
|
||||||
* Папка — пока с сервера (иконки папки в наборе дизайна нет; TODO забрать из Figma).
|
* Папка — фирменная folder-icon-colored.svg из боевой темы (тоже локально).
|
||||||
*/
|
*/
|
||||||
fun iconUrl(serverUrl: String, name: String, isDirectory: Boolean): String {
|
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"
|
return "file:///android_asset/filetypes/${localIcon(name)}.svg"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,34 +4,32 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
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.padding
|
||||||
import androidx.compose.foundation.layout.size
|
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.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.List
|
import androidx.compose.material.icons.filled.ChevronRight
|
||||||
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.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
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.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
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.F7Radius
|
||||||
import ru.forbion.f7cloud.core.designsystem.F7Spacing
|
import ru.forbion.f7cloud.core.designsystem.F7Spacing
|
||||||
|
|
||||||
|
// Размеры — из боевой темы forbion (_mobile-files.css), копия один в один.
|
||||||
|
|
||||||
enum class FilesViewMode { List, Grid }
|
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
|
@Composable
|
||||||
fun FilesViewToggleButton(viewMode: FilesViewMode, onToggle: () -> Unit) {
|
fun FilesViewToggleButton(viewMode: FilesViewMode, onToggle: () -> Unit) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(40.dp)
|
.size(40.dp)
|
||||||
.clip(RoundedCornerShape(F7Radius.md))
|
.f7PillSurface(RoundedCornerShape(F7Radius.pill))
|
||||||
.background(F7Colors.SecondaryButtonBg)
|
|
||||||
.border(1.dp, F7Colors.Border, RoundedCornerShape(F7Radius.md))
|
|
||||||
.clickable(onClick = onToggle),
|
.clickable(onClick = onToggle),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Icon(
|
AsyncImage(
|
||||||
imageVector = if (viewMode == FilesViewMode.List) Icons.Filled.GridView else Icons.AutoMirrored.Filled.List,
|
model = FileIcons.chromeAsset(
|
||||||
|
if (viewMode == FilesViewMode.List) "grid-files-gray.svg" else "list-view-icon.svg",
|
||||||
|
),
|
||||||
contentDescription = "Сменить вид",
|
contentDescription = "Сменить вид",
|
||||||
tint = F7Colors.TextSecondary,
|
modifier = Modifier.size(16.dp),
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Хлебные крошки: домой + сегменты пути через ›, прокрутка по горизонтали (макет F-21/F-22). */
|
/** Хлебные крошки: дом 16 + сегменты (все — серые 14/20) через шеврон 16, h-прокрутка. */
|
||||||
@Composable
|
@Composable
|
||||||
fun FilesBreadcrumb(
|
fun FilesBreadcrumb(
|
||||||
segments: List<String>,
|
segments: List<String>,
|
||||||
@@ -75,23 +102,28 @@ fun FilesBreadcrumb(
|
|||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.horizontalScroll(rememberScrollState())
|
.horizontalScroll(rememberScrollState())
|
||||||
.padding(vertical = F7Spacing.sm),
|
.padding(horizontal = F7Spacing.sm, vertical = F7Spacing.xs),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(F7Spacing.xs),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Icon(
|
AsyncImage(
|
||||||
imageVector = Icons.Filled.Home,
|
model = FileIcons.chromeAsset("home-bottom.svg"),
|
||||||
contentDescription = "В начало",
|
contentDescription = "В начало",
|
||||||
tint = F7Colors.TextSecondary,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(22.dp)
|
.size(16.dp)
|
||||||
.clickable(onClick = onHomeClick),
|
.clickable(onClick = onHomeClick),
|
||||||
)
|
)
|
||||||
segments.forEachIndexed { i, seg ->
|
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(
|
||||||
text = seg,
|
text = seg,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = if (i == segments.lastIndex) F7Colors.TextPrimary else F7Colors.TextSecondary,
|
color = F7Colors.TextSecondary,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
modifier = Modifier.clickable { onSegmentClick(i) },
|
modifier = Modifier.clickable { onSegmentClick(i) },
|
||||||
)
|
)
|
||||||
@@ -99,7 +131,7 @@ fun FilesBreadcrumb(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ряд фильтр-чипов: Тип файла / Участники / Изменен (макет F-18/F-22). */
|
/** Ряд фильтр-чипов: Тип файла / Участники / Изменен (пилюли 25dp, minWidth 86). */
|
||||||
@Composable
|
@Composable
|
||||||
fun FilesFilterChips(
|
fun FilesFilterChips(
|
||||||
onTypeClick: () -> Unit,
|
onTypeClick: () -> Unit,
|
||||||
@@ -111,7 +143,7 @@ fun FilesFilterChips(
|
|||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.horizontalScroll(rememberScrollState())
|
.horizontalScroll(rememberScrollState())
|
||||||
.padding(vertical = F7Spacing.xs),
|
.padding(horizontal = F7Spacing.xs, vertical = F7Spacing.xs),
|
||||||
horizontalArrangement = Arrangement.spacedBy(F7Spacing.sm),
|
horizontalArrangement = Arrangement.spacedBy(F7Spacing.sm),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
@@ -125,16 +157,21 @@ fun FilesFilterChips(
|
|||||||
private fun FilterChip(label: String, onClick: () -> Unit) {
|
private fun FilterChip(label: String, onClick: () -> Unit) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(F7Radius.pill))
|
.height(25.dp)
|
||||||
.border(1.dp, F7Colors.Border, RoundedCornerShape(F7Radius.pill))
|
.widthIn(min = 86.dp)
|
||||||
|
.f7PillSurface(RoundedCornerShape(F7Radius.pill))
|
||||||
.clickable(onClick = onClick)
|
.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
|
@Composable
|
||||||
fun FilesGridCard(
|
fun FilesGridCard(
|
||||||
serverUrl: String,
|
serverUrl: String,
|
||||||
@@ -146,41 +183,40 @@ fun FilesGridCard(
|
|||||||
onMenuClick: () -> Unit,
|
onMenuClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val shape = RoundedCornerShape(12.dp)
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.clip(RoundedCornerShape(F7Radius.lg))
|
.clip(shape)
|
||||||
.background(if (selected) F7Colors.PrimaryLight else F7Colors.Surface)
|
.background(if (selected) F7Colors.PrimaryLight else F7Colors.Background)
|
||||||
.border(1.dp, F7Colors.Border, RoundedCornerShape(F7Radius.lg))
|
.border(1.dp, F7Colors.Border, shape)
|
||||||
.clickable(onClick = onOpenClick)
|
.clickable(onClick = onOpenClick)
|
||||||
.padding(F7Spacing.md),
|
.padding(12.dp),
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||||
Checkbox(
|
F7ThemeCheckbox(checked = selected, onToggle = onSelectedChange)
|
||||||
checked = selected,
|
|
||||||
onCheckedChange = { onSelectedChange() },
|
|
||||||
colors = CheckboxDefaults.colors(checkedColor = F7Colors.Primary),
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(F7Spacing.sm))
|
|
||||||
Box(modifier = Modifier.weight(1f))
|
Box(modifier = Modifier.weight(1f))
|
||||||
Icon(
|
if (favorite) {
|
||||||
imageVector = if (favorite) Icons.Filled.Star else Icons.Filled.StarBorder,
|
AsyncImage(
|
||||||
contentDescription = null,
|
model = FileIcons.chromeAsset("star-green-full.svg"),
|
||||||
tint = if (favorite) F7Colors.Primary else F7Colors.TextMuted,
|
contentDescription = "В избранном",
|
||||||
modifier = Modifier.size(18.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(F7Spacing.sm))
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = F7Spacing.lg),
|
.height(80.dp),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = FileIcons.iconUrl(serverUrl, item.name, item.isDirectory),
|
model = FileIcons.iconUrl(serverUrl, item.name, item.isDirectory),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(48.dp),
|
modifier = Modifier.size(80.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Spacer(Modifier.height(F7Spacing.sm))
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||||
Text(
|
Text(
|
||||||
text = item.name,
|
text = item.name,
|
||||||
@@ -191,12 +227,11 @@ fun FilesGridCard(
|
|||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
Icon(
|
AsyncImage(
|
||||||
imageVector = Icons.Filled.MoreVert,
|
model = FileIcons.chromeAsset("dots-vertic-gray.svg"),
|
||||||
contentDescription = "Меню",
|
contentDescription = "Меню",
|
||||||
tint = F7Colors.TextSecondary,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(18.dp)
|
.size(16.dp)
|
||||||
.clickable(onClick = onMenuClick),
|
.clickable(onClick = onMenuClick),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -221,7 +256,7 @@ fun FilesEmptyState(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(72.dp)
|
.size(72.dp)
|
||||||
.clip(RoundedCornerShape(F7Radius.pill))
|
.clip(CircleShape)
|
||||||
.background(F7Colors.SurfaceMuted),
|
.background(F7Colors.SurfaceMuted),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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.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
|
||||||
import androidx.compose.foundation.layout.width
|
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.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -559,7 +561,6 @@ fun FilesSearchBar(
|
|||||||
onQueryChange: (String) -> Unit,
|
onQueryChange: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val base = serverUrl.trimEnd('/')
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.height(40.dp)
|
.height(40.dp)
|
||||||
@@ -569,7 +570,7 @@ fun FilesSearchBar(
|
|||||||
spotColor = Color(0xFFCBCBCB),
|
spotColor = Color(0xFFCBCBCB),
|
||||||
)
|
)
|
||||||
.clip(RoundedCornerShape(100.dp))
|
.clip(RoundedCornerShape(100.dp))
|
||||||
.background(Color(0xFFFDFDFD))
|
.background(F7Colors.Background)
|
||||||
.border(1.dp, F7Colors.Border, RoundedCornerShape(100.dp)),
|
.border(1.dp, F7Colors.Border, RoundedCornerShape(100.dp)),
|
||||||
contentAlignment = Alignment.CenterStart,
|
contentAlignment = Alignment.CenterStart,
|
||||||
) {
|
) {
|
||||||
@@ -581,7 +582,7 @@ fun FilesSearchBar(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = "$base/themes/forbion/images/header/search-gray.svg",
|
model = FileIcons.chromeAsset("search-gray.svg"),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
contentScale = ContentScale.Fit,
|
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 hasExt = !item.isDirectory && item.name.contains('.')
|
||||||
val baseName = if (hasExt) item.name.substringBeforeLast('.') else item.name
|
val baseName = if (hasExt) item.name.substringBeforeLast('.') else item.name
|
||||||
val extension = if (hasExt && showExtensions) "." + item.name.substringAfterLast('.') else ""
|
val extension = if (hasExt && showExtensions) "." + item.name.substringAfterLast('.') else ""
|
||||||
|
// Строка 44dp по боевой теме (_mobile-files.css): чекбокс 16 в колонке 44, иконка 30
|
||||||
|
// (зелёная звезда 16 поверх у избранных), имя, размер 56 по центру, дата 88 вправо, ⋮ в 32.
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(F7Radius.md))
|
.height(44.dp)
|
||||||
// выбранная строка — светло-зелёный фон (макет F-21)
|
|
||||||
.background(if (selected) F7Colors.PrimaryLight else Color.Transparent)
|
.background(if (selected) F7Colors.PrimaryLight else Color.Transparent)
|
||||||
.clickable(onClick = onOpenClick)
|
.clickable(onClick = onOpenClick),
|
||||||
.padding(horizontal = F7Spacing.sm, vertical = 10.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Checkbox(
|
Box(modifier = Modifier.size(44.dp), contentAlignment = Alignment.Center) {
|
||||||
checked = selected,
|
F7ThemeCheckbox(checked = selected, onToggle = onSelectedChange)
|
||||||
onCheckedChange = { onSelectedChange() },
|
}
|
||||||
colors = CheckboxDefaults.colors(checkedColor = F7Colors.Primary),
|
Box(modifier = Modifier.size(30.dp)) {
|
||||||
)
|
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = FileIcons.iconUrl(serverUrl, item.name, item.isDirectory),
|
model = FileIcons.iconUrl(serverUrl, item.name, item.isDirectory),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(24.dp),
|
modifier = Modifier.size(30.dp),
|
||||||
contentScale = ContentScale.Fit,
|
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(
|
||||||
text = buildAnnotatedString {
|
text = buildAnnotatedString {
|
||||||
append(baseName)
|
append(baseName)
|
||||||
@@ -823,17 +843,38 @@ fun FilesListRow(
|
|||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(horizontal = F7Spacing.sm),
|
.padding(start = F7Spacing.sm, end = F7Spacing.xs),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = F7Colors.TextPrimary,
|
color = F7Colors.TextPrimary,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
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(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(36.dp)
|
.width(32.dp)
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.height(44.dp)
|
||||||
.clickable(
|
.clickable(
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
indication = null,
|
indication = null,
|
||||||
@@ -845,7 +886,7 @@ fun FilesListRow(
|
|||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = "${serverUrl.trimEnd('/')}/themes/forbion/images/files/dots-vertic-gray.svg",
|
model = FileIcons.chromeAsset("dots-vertic-gray.svg"),
|
||||||
contentDescription = "Действия",
|
contentDescription = "Действия",
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
contentScale = ContentScale.Fit,
|
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
|
@Composable
|
||||||
fun FileActionMenuPopup(
|
fun FileActionMenuPopup(
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
|
|||||||