File Item
A file the app took, in a row: its icon, its name, its size or what went wrong with it, how far it is sent, and the button that takes it away. The rows stand under a PantrixFileDrop, each for a file the app keeps.
import com.pantrixui.components.elements.filedrop.PantrixFileItem
- Pressing works everywhere. Pressing the zone (or Enter or Space while it has the focus) opens the platform's picker: the documents on Android, Files on iOS, the system's window on the desktop, the browser's on the web. Where only pictures are taken, Android and iOS open the photo picker. None of them asks for a permission.
- Dropping is a pointer's work. On the desktop and the web files are dragged over the zone and dropped; on Android and the iPad from an app beside it too. While something is dragged over it the zone turns the primary role's colour and says "Drop to add". Its words follow suit: on the desktop and the web they put dropping first, on a phone choosing.
- Screening.
acceptsets the kinds taken,maxSizethe largest size,multiplemore than one file. Files that do not fit go toonRejectedwith their reasons (kind, size, number), the rest toonFiles; what to say is the app's. - The zone keeps no state. The app keeps the files taken and lists them under the zone as
PantrixFileItemrows: the name (its middle cut when long), the size in the language's way, the remove button, an optional progress and an error. - Reading. A
PantrixFilecarries its name, size and type, and reads its bytes whenreadBytes()asks. The platform's own file is there too:urion Android,urlon iOS,fileon the desktop,domFileon the web. - Platform. The zone is 140dp tall on a phone and 120dp on the desktop and the web; the rows are 56dp on a phone and 44dp under a pointer.
Examples
Attachments: kinds, size and rows
A form's attachments: PDF or pictures, several, each at most 10 MB. Every file taken becomes a row and gets a bar that fills as if it were being sent; the remove button deletes the row. The ones not taken are written in the supporting text with their reasons.
val attachments = remember { mutableStateListOf<PantrixFile>() }
val sent = remember { mutableStateMapOf<PantrixFile, Float>() }
var refused by remember { mutableStateOf(emptyList<RejectedFile>()) }
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
PantrixFileDrop(
onFiles = { files ->
attachments += files
refused = emptyList()
},
accept = listOf(PantrixFileType.Pdf, PantrixFileType.Images),
multiple = true,
maxSize = TEN_MEGABYTES,
onRejected = { refused = it },
supportingText = if (refused.isEmpty()) "PDF or a picture, each at most 10 MB" else reasonOf(refused),
error = refused.isNotEmpty(),
tagKey = "attachments"
)
attachments.forEach { file ->
key(file) {
SendingLater(file, sent)
PantrixFileItem(
name = file.name,
size = file.size,
onRemove = {
attachments -= file
sent -= file
},
progress = sent[file]?.takeIf { it < 1f },
tagKey = file.name
)
}
}
}
@Composable
private fun SendingLater(
file: PantrixFile,
sent: MutableMap<PantrixFile, Float>
) {
LaunchedEffect(file) {
if (file in sent) return@LaunchedEffect
repeat(STEPS + 1) { step ->
sent[file] = step / STEPS.toFloat()
delay(STEP_MILLIS)
}
}
}
private fun reasonOf(refused: List<RejectedFile>): String = refused.groupBy { it.reason }.entries.joinToString(" · ") {
val names = it.value.joinToString { rejected -> rejected.file.name }
when (it.key) {
FileRejection.Type -> "This kind is not taken: $names"
FileRejection.Size -> "Too large: $names"
FileRejection.Count -> "Only one file is taken: $names"
}
}
private const val TEN_MEGABYTES = 10_000_000L
private const val STEPS = 10
private const val STEP_MILLIS = 150L
One picture and the reason it was refused
One picture, at most 2 MB: a profile picture. On a phone the photo picker opens. The chosen picture is read and shown; a larger one, or a file that is no picture, is refused, and why is written under the zone as an error.
var avatar by remember { mutableStateOf<PantrixFile?>(null) }
var refused by remember { mutableStateOf<RejectedFile?>(null) }
var picture by remember { mutableStateOf<ImageBitmap?>(null) }
LaunchedEffect(avatar) {
// The bytes are read only when asked for: here, once the zone's work is done.
picture = avatar?.let { file -> runCatching { file.readBytes().decodeToImageBitmap() }.getOrNull() }
}
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
PantrixFileDrop(
onFiles = { files ->
avatar = files.single()
refused = null
},
accept = listOf(PantrixFileType.Images),
maxSize = TWO_MEGABYTES,
onRejected = { refused = it.first() },
label = "Choose a profile picture",
supportingText = refused?.let { reasonOf(listOf(it)) } ?: "PNG, JPEG or WebP, at most 2 MB",
error = refused != null,
tagKey = "avatar"
)
picture?.let {
PantrixImage(bitmap = it, contentDescription = "The chosen picture", modifier = Modifier.size(PICTURE_SIZE))
}
avatar?.let { PantrixFileItem(name = it.name, size = it.size, onRemove = { avatar = null }, tagKey = "avatar") }
}
private fun reasonOf(refused: List<RejectedFile>): String = refused.groupBy { it.reason }.entries.joinToString(" · ") {
val names = it.value.joinToString { rejected -> rejected.file.name }
when (it.key) {
FileRejection.Type -> "This kind is not taken: $names"
FileRejection.Size -> "Too large: $names"
FileRejection.Count -> "Only one file is taken: $names"
}
}
private val PICTURE_SIZE = 96.dp
private const val TWO_MEGABYTES = 2_000_000L
A low area and dense rows in their own style
In their own style: FileDropStyle gives the drop area's height, its dashed line and its corners, FileItemStyle a file row's height and its progress line. Every field not given is the platform's preset.
val files = remember { mutableStateListOf<PantrixFile>() }
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.xs)) {
PantrixFileDrop(
onFiles = { files += it },
multiple = true,
style = FileDropStyle(minHeight = 72.dp, shape = ShapeRole.Large, dashLength = 4.dp, dashGap = 4.dp),
tagKey = "low"
)
files.forEach { file ->
key(file) {
PantrixFileItem(
name = file.name,
size = file.size,
onRemove = { files -= file },
style = FileItemStyle(minHeight = 40.dp, verticalPadding = 4.dp, progressHeight = 2.dp),
tagKey = file.name
)
}
}
}
API
name
A file the app took, in a row: its icon, its name, its size or what went wrong with it, how far it is sent, and the button that takes it away. The rows stand under a PantrixFileDrop, each for a file the app keeps.
| Parameter | Type | Default | What it is |
|---|---|---|---|
name | String | what the file is called. | |
modifier | Modifier | Modifier | the row's modifier. The row fills the width it is given. |
size | Long? | null | how many bytes the file has, or null to say nothing of it. |
onRemove | (() -> Unit)? | null | what the remove button does, or null for a row with no button. |
progress | Float? | null | how much of the file is sent, from nought to one, or null for no bar. |
error | String? | null | what went wrong with the file, in the place of its size, or null. |
icon | ImageVector | PantrixIcons.File | what stands before the name: a file by default. |
enabled | Boolean | true | whether the remove button can be pressed. A row that cannot is drawn faint. |
style | FileItemStyle? | null | this call's own style: over every enclosing one and under the density. Only the fields it sets change, on every platform. |
tagKey | String? | null | the row's automation key: it is FileItem_<tagKey>, and its button's IconButton_remove inside it. Tagged only when given one. |
FileItemStyle
PantrixFileItem's geometry: a row for a file the app took, with its icon, its name and size, and the button that takes it away.
| Parameter | Type | Default | What it is |
|---|---|---|---|
shape | ShapeSpec | ShapeSpec.Unspecified | The row's corners. |
minHeight | Dp | Dp.Unspecified | The least height of the row. A larger type, a progress bar or an error makes it taller. |
horizontalPadding | Dp | Dp.Unspecified | The room between the row's sides and what it holds. |
verticalPadding | Dp | Dp.Unspecified | The room above and below what it holds. |
borderWidth | Dp | Dp.Unspecified | The width of the row's edge. |
iconSize | Dp | Dp.Unspecified | The width and height of the file's icon. |
gap | Dp | Dp.Unspecified | Between the icon, the words and the button, and between the words and the progress bar. |
progressHeight | Dp | Dp.Unspecified | The height of the progress bar under the words. |
Accessibility
- Screen reader. The whole zone is one button. How many files were taken and how many were not is announced politely; with
errorthe supporting text is read as the error.