Pantrix UI

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

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.

ParameterTypeDefaultWhat it is
nameStringwhat the file is called.
modifierModifierModifierthe row's modifier. The row fills the width it is given.
sizeLong?nullhow many bytes the file has, or null to say nothing of it.
onRemove(() -> Unit)?nullwhat the remove button does, or null for a row with no button.
progressFloat?nullhow much of the file is sent, from nought to one, or null for no bar.
errorString?nullwhat went wrong with the file, in the place of its size, or null.
iconImageVectorPantrixIcons.Filewhat stands before the name: a file by default.
enabledBooleantruewhether the remove button can be pressed. A row that cannot is drawn faint.
styleFileItemStyle?nullthis call's own style: over every enclosing one and under the density. Only the fields it sets change, on every platform.
tagKeyString?nullthe 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.

ParameterTypeDefaultWhat it is
shapeShapeSpecShapeSpec.UnspecifiedThe row's corners.
minHeightDpDp.UnspecifiedThe least height of the row. A larger type, a progress bar or an error makes it taller.
horizontalPaddingDpDp.UnspecifiedThe room between the row's sides and what it holds.
verticalPaddingDpDp.UnspecifiedThe room above and below what it holds.
borderWidthDpDp.UnspecifiedThe width of the row's edge.
iconSizeDpDp.UnspecifiedThe width and height of the file's icon.
gapDpDp.UnspecifiedBetween the icon, the words and the button, and between the words and the progress bar.
progressHeightDpDp.UnspecifiedThe height of the progress bar under the words.

Accessibility