OXYGENHUB
# 23279b95b5b2 oxygenhub-android / app / src / main / java / io / oxygenhub / android / ui / common / States.kt
4.1 KiB · last changed The quiet field behind empty and error screens, and screens that slide in from where they lead (refs #23) 8 days ago source blame history raw
1
package io.oxygenhub.android.ui.common
2
3
import android.text.format.DateUtils
4
import androidx.compose.foundation.layout.Arrangement
5
import androidx.compose.foundation.layout.Box
6
import androidx.compose.foundation.layout.Column
7
import androidx.compose.foundation.layout.Spacer
8
import androidx.compose.foundation.layout.fillMaxSize
9
import androidx.compose.foundation.layout.height
10
import androidx.compose.foundation.layout.padding
11
import androidx.compose.foundation.layout.size
12
import androidx.compose.material.icons.Icons
13
import androidx.compose.material.icons.outlined.CloudOff
14
import androidx.compose.material3.Button
15
import androidx.compose.material3.CircularProgressIndicator
16
import androidx.compose.material3.ExperimentalMaterial3Api
17
import androidx.compose.material3.Icon
18
import androidx.compose.material3.MaterialTheme
19
import androidx.compose.material3.Text
20
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
21
import androidx.compose.runtime.Composable
22
import androidx.compose.ui.Alignment
23
import androidx.compose.ui.Modifier
24
import androidx.compose.ui.graphics.vector.ImageVector
25
import androidx.compose.ui.text.style.TextAlign
26
import androidx.compose.ui.unit.dp
27
import io.oxygenhub.android.ui.backdrop.Backdrop
28
import uniffi.rust_core.Backdrop as Scene
29
30
/** A spinner in the middle of the space. */
31
@Composable
32
fun LoadingState(modifier: Modifier = Modifier) {
33
    Box(modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
34
        CircularProgressIndicator()
35
    }
36
}
37
38
/** Nothing to show, said plainly, with an icon. */
39
@Composable
40
fun EmptyState(
41
    icon: ImageVector,
42
    title: String,
43
    body: String,
44
    modifier: Modifier = Modifier,
45
    action: (@Composable () -> Unit)? = null,
46
) {
47
    Message(icon, title, body, modifier) {
48
        if (action != null) {
49
            Spacer(Modifier.height(8.dp))
50
            action()
51
        }
52
    }
53
}
54
55
/** What went wrong, and a way to try again. */
56
@Composable
57
fun ErrorState(message: String, onRetry: () -> Unit, modifier: Modifier = Modifier) {
58
    Message(Icons.Outlined.CloudOff, "Couldn't load this", message, modifier) {
59
        Spacer(Modifier.height(16.dp))
60
        Button(onClick = onRetry) { Text("Try again") }
61
    }
62
}
63
64
@Composable
65
private fun Message(
66
    icon: ImageVector,
67
    title: String,
68
    body: String,
69
    modifier: Modifier = Modifier,
70
    extra: @Composable () -> Unit = {},
71
) {
72
    // The quiet field behind it, as behind the hub's error pages (#23).
73
    Backdrop(key = Unit, make = { still -> Scene.adrift(still) }, modifier = modifier.fillMaxSize()) {
74
        Column(
75
            Modifier.fillMaxSize().padding(32.dp),
76
            horizontalAlignment = Alignment.CenterHorizontally,
77
            verticalArrangement = Arrangement.Center,
78
        ) {
79
            Icon(icon, contentDescription = null, modifier = Modifier.size(48.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant)
80
            Spacer(Modifier.height(16.dp))
81
            Text(title, style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center)
82
            Spacer(Modifier.height(8.dp))
83
            Text(body, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center)
84
            extra()
85
        }
86
    }
87
}
88
89
/**
90
 * The usual body of a screen over a [Load]: a spinner, the error with a
91
 * retry, or [content] in a pull-to-refresh box.
92
 */
93
@OptIn(ExperimentalMaterial3Api::class)
94
@Composable
95
fun <T> LoadContent(
96
    load: Load<T>,
97
    onRefresh: () -> Unit,
98
    modifier: Modifier = Modifier,
99
    content: @Composable (T) -> Unit,
100
) {
101
    when (load) {
102
        Load.Loading -> LoadingState(modifier)
103
        is Load.Failed -> ErrorState(load.message, onRefresh, modifier)
104
        is Load.Ready -> PullToRefreshBox(isRefreshing = load.refreshing, onRefresh = onRefresh, modifier = modifier.fillMaxSize()) {
105
            content(load.value)
106
        }
107
    }
108
}
109
110
/** "3 hours ago", from milliseconds since the epoch. */
111
fun relativeTime(ms: Long, now: Long = System.currentTimeMillis()): String =
112
    if (ms <= 0) "" else DateUtils.getRelativeTimeSpanString(ms, now, DateUtils.MINUTE_IN_MILLIS).toString()