OXYGENHUB
# 645469f5227b oxygenhub-android / app / src / main / java / io / oxygenhub / android / ui / common / States.kt
4.1 KiB · last changed Colour on icons: what each stands for, in the hub's colours (closes #25) 7 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 io.oxygenhub.android.ui.theme.Tones
29
import uniffi.rust_core.Backdrop as Scene
30
31
/** A spinner in the middle of the space. */
32
@Composable
33
fun LoadingState(modifier: Modifier = Modifier) {
34
    Box(modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
35
        CircularProgressIndicator()
36
    }
37
}
38
39
/** Nothing to show, said plainly, with an icon. */
40
@Composable
41
fun EmptyState(
42
    icon: ImageVector,
43
    title: String,
44
    body: String,
45
    modifier: Modifier = Modifier,
46
    action: (@Composable () -> Unit)? = null,
47
) {
48
    Message(icon, title, body, modifier) {
49
        if (action != null) {
50
            Spacer(Modifier.height(8.dp))
51
            action()
52
        }
53
    }
54
}
55
56
/** What went wrong, and a way to try again. */
57
@Composable
58
fun ErrorState(message: String, onRetry: () -> Unit, modifier: Modifier = Modifier) {
59
    Message(Icons.Outlined.CloudOff, "Couldn't load this", message, modifier) {
60
        Spacer(Modifier.height(16.dp))
61
        Button(onClick = onRetry) { Text("Try again") }
62
    }
63
}
64
65
@Composable
66
private fun Message(
67
    icon: ImageVector,
68
    title: String,
69
    body: String,
70
    modifier: Modifier = Modifier,
71
    extra: @Composable () -> Unit = {},
72
) {
73
    // The quiet field behind it, as behind the hub's error pages (#23).
74
    Backdrop(key = Unit, make = { still -> Scene.adrift(still) }, modifier = modifier.fillMaxSize()) {
75
        Column(
76
            Modifier.fillMaxSize().padding(32.dp),
77
            horizontalAlignment = Alignment.CenterHorizontally,
78
            verticalArrangement = Arrangement.Center,
79
        ) {
80
            Icon(icon, contentDescription = null, modifier = Modifier.size(48.dp), tint = Tones.info.copy(alpha = 0.85f))
81
            Spacer(Modifier.height(16.dp))
82
            Text(title, style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center)
83
            Spacer(Modifier.height(8.dp))
84
            Text(body, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center)
85
            extra()
86
        }
87
    }
88
}
89
90
/**
91
 * The usual body of a screen over a [Load]: a spinner, the error with a
92
 * retry, or [content] in a pull-to-refresh box.
93
 */
94
@OptIn(ExperimentalMaterial3Api::class)
95
@Composable
96
fun <T> LoadContent(
97
    load: Load<T>,
98
    onRefresh: () -> Unit,
99
    modifier: Modifier = Modifier,
100
    content: @Composable (T) -> Unit,
101
) {
102
    when (load) {
103
        Load.Loading -> LoadingState(modifier)
104
        is Load.Failed -> ErrorState(load.message, onRefresh, modifier)
105
        is Load.Ready -> PullToRefreshBox(isRefreshing = load.refreshing, onRefresh = onRefresh, modifier = modifier.fillMaxSize()) {
106
            content(load.value)
107
        }
108
    }
109
}
110
111
/** "3 hours ago", from milliseconds since the epoch. */
112
fun relativeTime(ms: Long, now: Long = System.currentTimeMillis()): String =
113
    if (ms <= 0) "" else DateUtils.getRelativeTimeSpanString(ms, now, DateUtils.MINUTE_IN_MILLIS).toString()